vue源码手写实现(四):观察者模式实现自动渲染

  1. 1. 引言-观察者模式
  2. 2. Watcher
  3. 3. Dep

引言-观察者模式

截止目前为止,基本的响应式和AST和render函数都能实现。不过目前只能通过手动调用_update(_render)来实现data数据改变后的重新渲染。vue中data数据被操作改变后,会自动重新渲染页面,本期我们采用观察者模式来实现该功能。

先说说观察者模式,用借钱来打比方:一个债主有很多外债,每过一段时间就去找向他借钱的人讨钱,不过别人不一定有钱,所以老是白跑路。那么借钱的人就说,等我有钱了,我来找你。所有借钱的人都这样说,那么这些借钱的人就算是一个个“被观察者”,只要钱够了,就向“主目标(观察者)”触发还钱的操作(程序上一般会触发观察者的update方法)。

回到我们源码里渲染的情况,主目标就是一个负责渲染的函数,被观察者就是data里会改变的值。当data里的某个值改变之后,就给渲染函数发出通知,渲染函数收到通知后执行渲染,也就是_update(_render)

我们给渲染抽象为Watcher类,data里改动的值作为观察者新增Dep类。

Watcher

watcher的代码顺序在compileToFunction之后,使用mountComponent单独处理这一段逻辑。

watcher放入三个参数:vm实例、渲染更新函数、渲染watcher标识。在类中增加id标识;增加dep数组以及addDep方法,用于存储多个dep。

增加update方法,这个方法提供给Dep类进行通知,并且最终真正执行更新的方法。这里的更新使用队列维护,设置防抖使其最终只更新一次,并且要实现异步更新。这里的异步做了兼容性处理,可以参考以下代码。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import Dep, { popStack, pushStack } from "./dep";


let id = 0;

class Watcher{// 不同的的组件有不同的watcher
constructor(vm,fn,options){
this.id = id++;

this.renderWatcher = options;// 标识是一个渲染watcher
this.getter = fn;// getter会发生取值操作

this.deps = [];
this.depsId = new Set();

this.get();
}

get(){
// Dep.target = this;// 静态属性,只有一份
pushStack(this);
this.getter();// 会去vm上取值,vm._update(vm._render())
// Dep.target = null;// 渲染完毕后就清空
popStack();
}

addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this);// watcher已经记住了dep并且已经去重,此时让dep记住watcher即可
}
}

update(){// 执行更新
// this.get();// 重新渲染
queueWatcher(this);// 把当前的watcher保存起来
}

run(){
this.get();// 真正执行渲染
}
}



// notify更新操作(异步)
let queue = [];
let has = {};
let pending = false;// 防抖

function flushSchedulerQueue(){
let flushQueue = queue.slice(0);
queue = [];
has = {};
pending = false;
flushQueue.forEach(q=>q.run());

}

function queueWatcher(watcher){
const id = watcher.id;
if(!has[id]){
queue.push(watcher);
has[id] = true;

// 不管update执行多少次,最终只会有一轮刷新操作
if(!pending){
nextTick(flushSchedulerQueue,0);
pending = true;
}
}
}


let callbacks = [];
let waiting = false;
function flushCallBacks(){
let cbs = callbacks.slice(0);
waiting = false;
callbacks = [];
cbs.forEach(cb=>cb());
}

// vue中nextTick 没有直接使用某个api 而是采用优雅降级的方式
//内部先采用的是promise(ie不兼容) Mutation0bserver 可以考虑ie专享的 setImmediate
// 使异步更新方案能兼容各种浏览器
let timerFunc;
if(Promise){
timerFunc = ()=>{
Promise.resolve().then(flushCallBacks);
}
}else if(MutationObserver){
let observer = new MutationObserver(flushCallBacks);// 这里回调是异步执行的
let textNode = document.createTextNode(1);
observer.observe(textNode,{
characterData:true,
});
timerFunc = ()=>{
textNode.textContent = 2;
}
}else if(setImmediate){
setImmediate(flushCallBacks);
}else{
timerFunc = ()=> {
setTimeout(flushCallBacks);
}
}

export function nextTick(cb){
callbacks.push(cb);
if(!waiting){
// setTimeout(()=>{
// flushCallBacks();
// },0)
timerFunc();
waiting = true;
}
}


export default Watcher

Dep

Dep类因为要绑定到data里改动的值上,所以要在Object.defineProperty里的set中新建,当值发生变化重新触发set就会新增一个Dep实例。

在class外部增加Dep.target = null,这里直接把属性放在类名上,其实是静态属性,所有实例都共用且不会被继承,这里我也踩了坑,需要注意下。这个target便是拿来存储watcher,当调用更新后重新赋值为null。

addSub():watcher中遍历dep时调用,让dep中存储watcher实例。

depend():让watcher添加dep实例(让watcher记住这个dep)。

notify():通知watcher进行更新。

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
let id = 0;

class Dep{
constructor(){
this.id = id++;
this.subs = [];// 这里存放着当前属性对应的watcher有哪些
}

depend(){
// 这里不希望放重复的watcher
// this.subs.push(Dep.target)
Dep.target.addDep(this);// 让watcher记住dep
}

addSub(Watcher){
this.subs.push(Watcher);
}

notify(){
this.subs.forEach(watcher => {
watcher.update()
})
}
}

Dep.target = null;

let stack = [];

export function pushStack(watcher){
stack.push(watcher);
Dep.target = watcher;
}

export function popStack(){
stack.pop();
Dep.target = stack[stack.length - 1];
}

export default Dep

下边是响应式核心处的代码,此前逻辑描述错误。

Dep是在get时就创建,这样的话data中的每个属性都会有一个Dep实例,当触发set时,data数据对应修改的dep就会调用notify通知watcher进行更新。而上边代码可以看到,watcher的更新是维护成队列的且添加防抖函数的,所以不会高频更新,而是一定时间内只会渲染一次。

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
export function defineReactive(target, key, value) {// 闭包 属性劫持
let childob = observe(key);// 如果仍旧是一个对象,则递归劫持 childOb.dep用来收集依赖

let dep = new Dep();
Object.defineProperty(target, key, {
get() {// 取值的时候会执行get
if(Dep.target){
dep.depend();// 让这个属性的收集器记住当前的watcher
if(childob){
childob.dep.depend();// 让数组和对象本身也能实现依赖收集
if(Array.isArray(value)){
dependArray(value);
}
}
}
return value;
},
set(newValue) {// 赋值的时候会执行set
if(newValue === value) return;
observe(newValue);
value = newValue;
dep.notify();
},
})
}