집필 통계2026
기술글자 수
올해 0자
누적 121,806자
기술글
올해 0편
누적 51편
생활글자 수
올해 3,986자
누적 132,785자
생활글
올해 1편
누적 27편
인기 태그

Vue 반응형 원리 및 구현

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

글의 배경

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

개요

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

흐름

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

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

주의사항

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

watcher이 getter을 트리거할 때, 현재 속성의 의존성 수집기를 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