Vue 반응형 원리 및 구현

✍🏼 작성일 2016년 08월 02일   
❗️ 참고: 이 글이 작성된 지 이미 일이 지났습니다. 시의성에 유의하세요

글의 배경

Vue의 반응형 시스템 원리를 연구해 보았고, 이를 기록하고 싶었습니다. 동시에 Vue의 반응형 시스템을 직접 구현해 보기로 했습니다. 그래서 이 글을 작성하게 되었습니다.

개요

Vue 반응형 데이터의 초기화는 initStateinitData에서 이루어집니다. observe 함수를 사용해 vm 객체의 data 속성을 관찰한 후, gettersetter 속성을 설정합니다.
따라서 최소한 세 가지 함수가 필요합니다: 속성 gettersetter를 관찰하는 함수, gettersetter를 트리거하는 监听者, 그리고 속성 의존성을 저장하는 收集框입니다. 저는 각각 observe, watcher, dep로 명명했습니다.

흐름

먼저 observe에서 data의 속성에 gettersetter을 재귀적으로 추가합니다. 이후 watcher에서 속성 중 하나에 접근할 때, watcher 인스턴스가 getter 인터셉터를 트리거합니다. 그러면 해당 속성의 인터셉터가 watcher 인스턴스를 해당 속성의 클로저 의존성 수집기 dep에 추가합니다. 동시에 이 클로저 의존성 수집기를 watcher에 넣습니다.

이후, 위에서 언급한 속성을 수정할 때 observe에 정의된 setter 인터셉터가 트리거됩니다. 이때 depwatcher이 작동하기 시작하여 해당 watcher의 콜백을 실행합니다.

주의사항

getter를 트리거할 때 의존성이 중복 수집되지 않도록 하는 방법은?

watchergetter을 트리거할 때, 현재 속성의 의존성 수집기를 watcher의 Set 배열에 추가하여 비교합니다. 중복되는 경우 더 이상 추가하지 않습니다:

1
2
3
4
5
6
7
addDep (dep) {
var id = dep.id;
if (!this.depIds.has(id)) { // 一次求值时候的去重
this.deps.push(dep);
dep.addSubs(this); // 再将 watcher 放到字段的 Dep 的实例 subs 中, 以供值变化的时候 执行 notity, 把 subs 中的 watcher 拉出来挨个执行一遍
}
}

setter를 트리거할 때 콜백을 어떻게 실행하나요?

setter을 트리거할 때, 해당 속성에 포함된 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
// setter
set (newVal) {
// 先触发一次依赖, 在 watcher 中 维护一个 depId 去重
if (newVal === val) {
console.log('值相等, 未变化, 呵呵');
return;
}
val = newVal;
dep.notity(); // 触发 值 watcher 的 update 操作
}
// dep.notity() 函数:
notity () {
this.subs.forEach((watcher) => {
watcher.update();
});
},
// watcher.update() 函数:
update () { // 触发值变化的时候, 执行该函数进行求值及更新操作
var value = this.get();
if (value !== this.value) {
console.log(`恭喜你成功更新了该值, 当前 deps 为: ${this.deps}, depsId 为: ${this.depIds}, 旧值为: ${this.value}, 新值为: ${value}`);
this.cb.call(this.data, this.key);
} else {
console.log('两次相同, 无需更改');
}
}

구체적인 코드:

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
var wId = 0; // watcher 自增 id
var dId = 0; // dep 自增 id
/**
@for 为了观察 data 上的属性变化
*/
function observe(data) {
var propertys = Object.keys(data);
var dep = new Dep(); // 闭包 Dep 外界不可访问, 实例的 subs 放着它的 watcher
propertys.forEach((property) => {
var val = data[property];
Object.defineProperty(data, property, {
get() {
// 添加依赖到 该属性的 闭包 dep 中
dep.depend();
return val;
},
set(newVal) {
// 先触发一次依赖, 在 watcher 中 维护一个 depId 去重
if (newVal === val) {
console.log('值相等, 未变化, 呵呵');
return;
}
val = newVal;
dep.notity(); // 触发 值 watcher 的 update 操作
},
});
});
}

/**
@for 收集 data 的 key 字段变化时候的响应, 并执行
*/
function Watcher(data, key, cb) {
this.data = data;
this.key = key;
this.cb = cb;
this.depIds = new Set();
this.deps = [];
this.value = this.get(); // 触发取值操作
}

Watcher.prototype = {
update() {
// 触发值变化的时候, 执行该函数进行求值及更新操作
var value = this.get();
if (value !== this.value) {
console.log(
`恭喜你成功更新了该值, 当前 deps 为: ${this.deps}, depsId 为: ${this.depIds}, 旧值为: ${this.value}, 新值为: ${value}`
);
this.cb.call(this.data, this.key);
} else {
console.log('两次相同, 无需更改');
}
},
get() {
var val;
pushTarget(this); // 为了方便, 设置 Dep.target 为当前 watcher 实例, 用完即删
val = this.data[this.key]; // 触发取值操作: 触发 observe 中的 getter => 触发 key 字段的 dep.depend() => 触发 watcher 的 addDep =>
popTarget(); // 用完弹出 后进先出
return val;
},
addDep(dep) {
var id = dep.id;
if (!this.depIds.has(id)) {
// 一次求值时候的去重
this.deps.push(dep);
dep.addSubs(this); // 再将 watcher 放到字段的 Dep 的实例 subs 中, 以供值变化的时候 执行 notity, 把 subs 中的 watcher 拉出来挨个执行一遍
}
},
};

/**
@for 收集 data 的 key 字段变化时候的响应, 并执行
*/
function Dep() {
this.subs = [];
this.id = wId++;
}
Dep.prototype = {
depend() {
Dep.target.addDep(this); // 触发 get 取值的时候, 将当前 dep 实例添加到 watcher 中
},
notity() {
this.subs.forEach((watcher) => {
watcher.update();
});
},
addSubs(watcher) {
this.subs.push(watcher);
},
};

var targetList = [];
function pushTarget(watcher) {
Dep.target = watcher;
targetList.push(watcher);
}
function popTarget() {
targetList.pop();
Dep.target = targetList[targetList.length - 1];
}

// 执行 watcher
var a = {
b: 'c',
};
observe(a);
// 这个 watcher 是用户自己写的 watch
new Watcher(a, 'b', function (data, key) {
console.log('watcher 回调成功执行!');
});

코드 실행 순서 다이어그램:

示意图

- EOF -
이 글의 최초 게시: Vue 반응형 원리 및 구현 - Xheldon Blog