Vue2 - 源码
yuiyake 8/9/2022
# Vue2 源码解读
# 源码链接
# 参考文档
- vue2源码解读 - 语雀 (opens new window)
- vue2生命周期详解 - CSDN (opens new window)
- 从源码的角度解读官网中的生命周期图 (opens new window)
# 基本思想
- vue的数据监听其实是用的观察者模式(不是发布订阅),事件总线eventBus才是用的发布订阅模式。观察者和发布订阅模式最大的区别就是观察者是强耦合,发布订阅是松散耦合。
- 观察者模式的主体关系是目标对象中通过observerList记录observer,属于强耦合(?)。
- vue2的数据双向绑定是用Object.defineProperty实现的,dep劫持数据得到对象属性的get和set,通过get set对它进行获取和修改操作。当get触发的时候, 说明view层有这个数据的依赖,然后收集这些关系;当set触发的时候,就要调用notify通知更新view。
- 观察者和发布订阅的区别
# 解读
vue2的class Observer 就是用来把数据处理成响应式:
- 判断对象是否是数组然后建立依赖关系 =>
- 数组走observeArray,非数组走walk =>
- 数组(observeArray)的话其实就是重写数组的方法(push,pop,shift,un-,splice,sort,reverse): 先调用原始方法操作,在做处理,最后调用dep通知。
- 非数组(defineReactive)的话就是遍历非数组对象的key去调用defineReactive(额,递归?),diff比对,如果数据被修改就调用dep.notify()通知
vue2中的class Dep 就是维护依赖关系的电话本。他链接observer和watcher(订阅者),最主要作用就是调用notify通知订阅者
- 具体的notify里干了什么,就是遍历observeArray(这里面叫subs)分别调用update去更新。
vue2中的class Watcher 是具体观察者(订阅者),它是接收通知并和 view 接触的。
- pushTarget:Dep方法,用来存放目标对象和设置标志位的
- value = this.getter.call(vm:vm): 从vm对象上取值,触发双绑数据(Observer)中的get方法。
- popTarget: 改变Dep.target标志。
更新机制:
- watcher 中的 update 方法调用了queueWatcher(),这个方法实际上和vue的dom更新机制有关,在这里最后调用了一个nextTick异步任务, 看到异步就应该知道nextTick的作用是什么了,就是在dom节点更新之后执行回调。
# 换个角度,从生命周期看呢?
- Vue2的生命周期分四大阶段,初始化,挂载,更新,销毁。
- 初始化的时候:beforeCreate是一个注入的流程(init),初始化props、methods、data、computed、watch。而created的时候vue的实例已经创建好, 但还是不能获得dom节点。在created会进行render()函数,这个函数就是依赖收集的。那对应哪里是依赖收集呢? 没错就是Dep。vue2用 Object.defineProperty()对属性进行数据劫持收集的依赖,交给Observer(这是后话了)。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
// 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
// 执行 beforeCreate 生命周期函数
callHook(vm, 'beforeCreate')
// 解析初始化当前组件的 inject
initInjections(vm) // resolve injections before data/props
// 初始化 state,包括 props、methods、data、computed、watch
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// 在 created 回调函数中,可以访问到实例中的数据
// 执行 created 回调函数
callHook(vm, 'created')
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 挂载阶段,beforeMount做的就是创建Watcher,触发patch函数,这个也很眼熟,哪里见到过呢?没错,在diff里。但由于目前还没有虚拟dom,所以会直接生成真实dom并挂上$el。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
// 执行 created 回调函数
callHook(vm, 'created')
// 如果配置中有 el 的话,则自动执行挂载操作
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 实现 vue2 响应式
function defineReactive(target, key, val) {
// a.传入参数的判断
observe(target);
// b. 数据劫持
Object.defineProperty(target, key, {
get() {
// 依赖处理
dep.depend();
return val;
},
set(newVal) {
// 派发更新
if(newVal !== val) {
observe((newVal));
val = newVal;
dep.notify()
}
}
})
// c. 数据后置操作 or 返回
}
// 对象嵌套 、 深层对象
function observe(obj) {
if(typeof obj !== 'object' || obj === null) return;
if(isArray(obj)) {
// 数组处理
Object.setPrototypeOf(obj, arrayProto);
} else {
const keys = Object.keys();
for(let i=0; i<keys.length; i++) {
const key = keys[i];
defineReactive(obj, key, obj[key]);
}
}
}
// 数组响应式
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);
['push', 'pop', 'splice', 'shift', 'unshift', 'reverse', 'sort'].forEach(key => {
arrayProto[push].apply(this.arguments);
notifyUpdate();
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43