本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。 这是源码共读的第23期,链接:为什么 Vue2 this 能够直接获取到 data 和 methods。
断点调试要领:
赋值语句可以一步按F10跳过,看返回值即可,后续详细再看。
函数执行需要断点按F11跟着看,也可以结合注释和上下文倒推这个函数做了什么。
有些不需要细看的,直接按F8走向下一个断点
刷新重新调试按F5
详见这篇文章《前端容易忽略的 debugger 调试技巧》,截图标注的很详细。
1. 准备调试
编辑器:VSCode
vue2源码地址:unpkg.com/vue@2.6.14/…
我这里是直接下载到本地,方便在本地调试
准备文件 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div>name:{{name}}</div>
<div>arr:{{JSON.stringify(arr)}}</div>
<div>obj:{{JSON.stringify(obj)}}</div>
</div>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<!-- <script src="./vue@2.6.14.js"></script> -->
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'Hello',
obj: { a: 1 },
arr: [{ a: 0 }, { a: 2 }]
},
methods: {
sayName() {
console.log(this.name);
},
changeName() {
this.name = 'world'
console.log(this.name);
},
},
});
console.log(vm.name);
</script>
</body>
</html>
VSCode 下载 Live Server 插件,就可以右击 index.html 文件,选 Open with Live Server 在浏览器打开调试
2. 数据侦听
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);
2.1 initMixin
进入 _init 在 initMixin 下
function initMixin (Vue) {
Vue.prototype._init = function (options) {
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
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);
}
};
}
可以看到在 _init 方法中:先调用了 mergeOptions() 方法合并实例构造函数和传入的options,感兴趣的童鞋可以自己再调试这部分源码。调用完后返回的 vm.$options 如下图:
接下来初始化生命周期,methods、data、watch、computed等。
下面详细看看 data 是如何定义的?
进入 initState,再进入 initData
这里 proxy工具方法代理,使用户通过 this.name 访问数据时,相当于 vm._data.name
2.2 Observer
往下进入 observe 方法
F11 从 new Observer(value) 进入首字母大写的 Observe 构造函数,传入 data。
var Observer = function Observer (value) { // 传入data对象
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) { // 数组
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else { // 对象
this.walk(value);
}
};
2.3 walk
这里我们可以看到对数组和对象分别进行数据侦听。如果是对象,则进入 Observer 原型对象的 walk 方法
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
2.4 defineReactive$$1
再进入 defineReactive$$1,在这里我们看到大家熟悉的 Object.defineProperty
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 递归子属性 observe 内部 new Observer()
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
new Dep() 时,在当前 dep 实例上创建 subs 数组,存放依赖项
并为对象中每个属性创建 get,set。
2.5 observeArray
遍历到data中的arr数组
Observer 中调用 protoAugment 通过使用__proto__拦截原型链来增加目标对象或数组,即重写数组 pop/push/reverse/shift/sort/splice/unshift 方法,通过 ob.dep.notify() 派发依赖
如果是数组,则遍历数组中的每个属性,进行
observe(items[i]) 监听
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
遍历到第一个元素 {a:0}
进入 defineReactive$$1,使用 Object.defineProperty 侦听对象
2.5.1 思考
思考:为什么在data中定义的数组arr,使用 this.arr[0] = {a:1} 这种下标修改的方式不生效?
初始化页面:
在控制台打印 vm.arr,可以看到arr数组两个元素的 a 属性都是 Observer 响应式被侦听的,有 get 和 set 方法。见下图:
接下来在控制台输入 vm.arr[0].a = 1,页面也响应式变化了,如下图:
但是如果我们直接用下标赋值的方式修改数据 vm.arr[0] = {a:1},页面不会响应式变化,且arr数组第一个元素丢失响应式
下面这段代码就能给出回答:因为 observeArray 方法中, observe(items[i]) 中监听的数组的每个属性值,对于数组本身是无法监听到的。通过 vm.arr[0] = {a:1} 表面上是修改了第一元素值,实际上 {} 相当于重新创建了一个新对象,和之前的对象内存地址不是同一个,他没有被observe侦听到,所以丢失响应式
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
这实际上也是 Object.defineProperty 的不足之处:
- 每次只能劫持一个属性,对于嵌套数组和对象,只能递归遍历;
- 对于对象的新增和修改属性无法监听的到,得用delete手动增加响应式;
- 对于数组也只能重写pop,push,reverse等方法。
3. 依赖收集
有三个问题: 依赖何时收集?依赖收集到哪里?依赖是谁? 下面一一解答。
Vue.prototype._init 方法中调用 vm.$mount(vm.$options.el);,返回 mountComponent 方法,在该方法中
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
3.1 Watcher
创建 Watcher
this.value = this.lazy
? undefined
: this.get();
Watcher 中调用 get 方法,求值getter,并重新收集依赖项
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm; //Vue实例
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// “touch”每个属性,使它们都被跟踪为对深度观察的依赖
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
pushTarget(this),this 指向当前 Watcher 实例,Dep.target 全局唯一。这里的 Watcher实例 就是我们收集的依赖
Watcher.prototype.get 再往下走,这里很巧妙,使用 this.getter.call(vm, vm) 读取值,触发getter,从而将this即Watcher实例添加到dep中
3.2 defineReactive$$1
我们看 defineReactive$$1方法,代码详解看注释
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep(); // 用 subs 数组存放依赖
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 递归子属性 observe 内部new Observer()
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend(); // 依赖收集
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify(); // 派发依赖
}
});
}
3.3 depend
dep.depend() 方法:
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
进入 addDep 方法
触发 addSub 方法,这里最终将 Watcher 添加到 subs 队列。这里 Watcher 起到一个桥梁纽带作用,当数据变化时通知 Watcher ,再由 Watcher 统一通知其他依赖。
执行完 mounted 钩子函数,模板编译完成,dom 树挂载。
_init 执行完毕
所以上面三个问题可以回答了。
- 依赖在
get数据时收集,即使用数据时,无论是模板中还是方法中; - 依赖收集在
Dep里面; - 依赖就是
Watcher实例。
4. 派发更新
控制台输入 vm.name = 'world',回车,进入 set
F11 进入 dep.notify()
遍历subs,即上面收集的依赖队列,调用 update(),告知依赖Watcher 数据发生变化
后续进行新旧Vnode比较,重新渲染。
总结
本文通过学习源码调试方法,对响应式原理的相关代码进行逐行逐段调试,能够增强原理的理解和掌握,遇到问题更容易debug问题所在点,并融会贯通其他框架源码的调试方法。
针对 vue2 中 Object.defineProperty 的缺陷,vue3 用 Proxy 一招解决所有问题,放弃了对低版本浏览器的兼容,从而达到更优的效果。