Skip to content

Commit c4b82b1

Browse files
committed
#35 에 대한 내용
1 parent cfe0e7f commit c4b82b1

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
---
2+
layout: post
3+
title: "Vue는 어떻게 data에 Observer / getter / setter 를 추가할까"
4+
date: 2017-01-21 13:36:00 +0900
5+
categories: jekyll update
6+
author: "ChangJoo Park"
7+
excerpt: "$data 의 observer와 getter, setter가 추가되는 과정을 알아봅니다."
8+
---
9+
10+
## Vue 인스턴스 / 컴포넌트 데이터에 붙어있는 것들
11+
12+
Vue.js 를 이용하여 개발하는 도중 `console.log` 를 이용하여 데이터를 출력하는 경우가 있습니다. 이때 실제 코드로 작성한 데이터 코드에는 없는 객체/함수들을 볼 수 있습니다.
13+
14+
이번에는 Observer / get / set 이 어떤 과정을 거쳐 사용자가 작성한 Vue의 데이터 객체에 추가되는지 살펴보겠습니다.
15+
16+
## 예제에서 사용할 Vue 앱
17+
18+
이번에 작성할 Vue 앱은 매우 간단합니다.
19+
20+
HTML 과 JS 파일 전체 입니다.
21+
22+
```html
23+
<!DOCTYPE html>
24+
<html>
25+
<head>
26+
<script src="../../dist/vue.js"></script>
27+
</head>
28+
<body>
29+
<div id="app">
30+
{{message}}
31+
</div>
32+
<script>
33+
var app = new Vue({
34+
el: '#app',
35+
data: function () {
36+
return {
37+
message: 'hello'
38+
}
39+
}
40+
})
41+
</script>
42+
</body>
43+
</html>
44+
45+
46+
```
47+
48+
`data` 는 키가 **message**이고 값이 **hello**인 객체를 가지게 됩니다. 위 앱을 실행한 후 브라우저에서 확인합니다.
49+
50+
![Imgur](http://i.imgur.com/Jzr8ENw.png)
51+
52+
`app`은 위 코드로 만든 Vue.js 앱입니다. 그리고 app이 가진 데이터는 `app.$data`로 확인할 수 있습니다.
53+
54+
데이터는 **message** 가 있고 값도 **hello** 로 정확하게 들어가 있습니다. 하지만 `__ob__``get`, `set` 을 발견할 수 있습니다.
55+
56+
실제 Vue 코드에서 어떤 과정을 거쳐 이들을 추가하는지 살펴보겠습니다.
57+
58+
59+
60+
## Vue 인스턴스 생성 과정 중 initData 메소드
61+
62+
결론부터 말하면 실제로 데이터를 처리하는 `initData` 메소드를 거쳐야 위 예제처럼 `__ob__`, `get`, `set` 이 추가됩니다. `vue/src/core/instance/state.js` 경로의 `initData` 코드를 따라가봅니다.
63+
64+
```javascript
65+
function initData(vm: Component) {
66+
let data = vm.$options.data
67+
data = vm._data = typeof data === 'function'
68+
? data.call(vm)
69+
: data || {}
70+
if (!isPlainObject(data)) {
71+
data = {}
72+
process.env.NODE_ENV !== 'production' && warn(
73+
'data functions should return an object:\n' +
74+
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
75+
vm
76+
)
77+
}
78+
// proxy data on instance
79+
const keys = Object.keys(data)
80+
const props = vm.$options.props
81+
let i = keys.length
82+
while (i--) {
83+
if (props && hasOwn(props, keys[i])) {
84+
process.env.NODE_ENV !== 'production' && warn(
85+
`The data property "${keys[i]}" is already declared as a prop. ` +
86+
`Use prop default value instead.`,
87+
vm
88+
)
89+
} else {
90+
proxy(vm, keys[i])
91+
}
92+
}
93+
// observe data
94+
observe(data, true /* asRootData */)
95+
}
96+
```
97+
98+
`initData` 메소드의 전체 코드입니다. 중요한 부분은 Vue 인스턴스 혹은 컴포넌트의 `data``function` 타입이 아닌 경우 처리하지 않습니다. 그리고 `props` 도 이 `initData`에서 처리합니다. 이번에는 data 에 대해서만 살펴보기 때문에 `props`에 대한 `proxy` 메소드는 지나갑니다. 맨 아래 줄의 `observe` 메소드가 호출되는 부분이 중요합니다.
99+
100+
101+
102+
## 데이터에 Observer 추가되는 과정
103+
104+
`observe`메소드는 `vue/src/core/observer/index.js`에 있습니다. 아래는 전체 코드입니다.
105+
106+
```javascript
107+
/**
108+
* 값에 대한 observer 인스턴스를 만들도록 시도하고,
109+
* 새로 만든 경우 새 observer를 반환합니다.
110+
* observer가 존재하는 경우 기존의 observer를 반환합니다.
111+
*/
112+
export function observe(value: any, asRootData: ?boolean): Observer | void {
113+
if (!isObject(value)) {
114+
return
115+
}
116+
let ob: Observer | void
117+
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
118+
ob = value.__ob__
119+
} else if (
120+
observerState.shouldConvert &&
121+
!isServerRendering() &&
122+
(Array.isArray(value) || isPlainObject(value)) &&
123+
Object.isExtensible(value) &&
124+
!value._isVue
125+
) {
126+
ob = new Observer(value)
127+
}
128+
if (asRootData && ob) {
129+
ob.vmCount++
130+
}
131+
return ob
132+
}
133+
```
134+
135+
Observer가 이미 존재하는 방법은 전달받은 `value``Observer` 타입의 `__ob__` 를 가지고 있는지 확인하는 것 입니다. 없는 경우 새로 만듭니다. Observer 객체를 만드는 조건을 통과하면 새 `Observer` 객체를 만들 수 있습니다. 객체를 만들 때 `value`는 Vue 인스턴스의 `data` 입니다.
136+
137+
>`observerState` 객체는 `shouldConvert` , `isSettingProps` 를 가집니다. 기본값은 각각 true, false 입니다. 이 때문에 인스턴스를 만드는 상황에서 `observerState`는 기본값을 가지므로 `Observer` 객체를 만들 수 있게 됩니다.
138+
139+
## Observer 클래스 살펴보기
140+
141+
Observer 인스턴스는 `value` 를 이용하여 만듭니다.
142+
143+
```javascript
144+
// Observer 클래스 생성자
145+
constructor(value: any) {
146+
this.value = value
147+
this.dep = new Dep()
148+
this.vmCount = 0
149+
150+
def(value, '__ob__', this)
151+
152+
if (Array.isArray(value)) {
153+
const augment = hasProto ? protoAugment : copyAugment
154+
augment(value, arrayMethods, arrayKeys)
155+
this.observeArray(value)
156+
} else {
157+
this.walk(value)
158+
}
159+
}
160+
```
161+
162+
살펴볼 부분은 `def(value, '__ob__', this)` 입니다. 익숙한 `__ob__` 가 보입니다. `def` 메소드의 코드 입니다. `def` 는 유틸리티 메소드 입니다. `Object.defineProperty` 를 이용하여 `__ob__` 키를 추가합니다.
163+
164+
```javascript
165+
/**
166+
* 속성을 정의합니다.
167+
*/
168+
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
169+
Object.defineProperty(obj, key, {
170+
value: val,
171+
enumerable: !!enumerable,
172+
writable: true,
173+
configurable: true
174+
})
175+
}
176+
```
177+
178+
179+
180+
이제 우리가 만든 Vue 앱의 data에 Observer가 추가 되었습니다. 이제 남은 것은 `get``set` 입니다.
181+
182+
Observer는 전달받은 값이 배열인 경우 `observeArray`를 호출하여 각 배열의 아이템에 대해 `observe` 메소드를 실행합니다. 이번에 만든 Vue 인스턴스의 데이터는 `{ message: 'hello' }` 이고 `'hello'`는 문자열이므로 `walk` 메소드를 실행합니다.
183+
184+
```javascript
185+
/**
186+
* 각 속성을 순회하여 getter/setter로 변환합니다.
187+
* 이 메소드는 타입이 Object인 경우에만 호출합니다.
188+
*/
189+
walk(obj: Object) {
190+
const keys = Object.keys(obj)
191+
for (let i = 0; i < keys.length; i++) {
192+
defineReactive(obj, keys[i], obj[keys[i]])
193+
}
194+
}
195+
```
196+
197+
이제 Vue 앱의 `data` 안에 있는 속성들을 순회하며 `defineReactive` 메소드를 실행하여 `getter/setter`를 추가합니다. 이를 통해 `console.log(app.$data)` 에서 나온 `get/set` 이 추가됩니다.
198+
199+
## getter와 setter를 추가하는 defineReactive
200+
201+
`defineReactive` 전체 코드 입니다.
202+
203+
```javascript
204+
/**
205+
* 객체에 반응형 속성을 정의합니다.
206+
*/
207+
export function defineReactive(
208+
obj: Object,
209+
key: string,
210+
val: any,
211+
customSetter?: Function
212+
) {
213+
const dep = new Dep()
214+
const property = Object.getOwnPropertyDescriptor(obj, key)
215+
if (property && property.configurable === false) {
216+
return
217+
}
218+
219+
// 사전 정의된 getter/setter 를 제공합니다.
220+
const getter = property && property.get
221+
const setter = property && property.set
222+
223+
let childOb = observe(val)
224+
Object.defineProperty(obj, key, {
225+
enumerable: true,
226+
configurable: true,
227+
get: function reactiveGetter() {
228+
const value = getter ? getter.call(obj) : val
229+
if (Dep.target) {
230+
dep.depend()
231+
if (childOb) {
232+
childOb.dep.depend()
233+
}
234+
if (Array.isArray(value)) {
235+
dependArray(value)
236+
}
237+
}
238+
return value
239+
},
240+
set: function reactiveSetter(newVal) {
241+
const value = getter ? getter.call(obj) : val
242+
/* eslint-disable no-self-compare */
243+
if (newVal === value || (newVal !== newVal && value !== value)) {
244+
return
245+
}
246+
/* eslint-enable no-self-compare */
247+
if (process.env.NODE_ENV !== 'production' && customSetter) {
248+
customSetter()
249+
}
250+
if (setter) {
251+
setter.call(obj, newVal)
252+
} else {
253+
val = newVal
254+
}
255+
childOb = observe(newVal)
256+
dep.notify()
257+
}
258+
})
259+
}
260+
```
261+
262+
`defineReactive` 메소드는 꽤 깁니다. 하지만 단순하게 `def`에서 보았던 `Object.defineProperty` 이용하여 `get``set` 속성을 추가합니다. `let childOb = observe(val)` 은 객체의 내부 속성까지 순회하면서 반응형 속성을 갖게 만들어 줍니다. 예제에서는 원시 문자열이므로 `undefined` 가 됩니다.
263+
264+
이 과정을 거쳐 Vue 인스턴스의 data에 `Observer`를 추가되고 data 내부 속성에 `get/set`이 추가됩니다. `data`에는 이번에 사용한 원시값 이외에도 배열 / 객체 사용할 수 있습니다. 이 경우는 직접 코드를 살펴보시면서 이해하실 수 있을 것으로 생각하여 다루지 않습니다. 어떠한 경우라도 Vue는 data 안에 있는 내용에 대해 상황에 따라 순회하며 `observer` 메소드를 실행합니다.
265+
266+
## 전체 과정
267+
268+
Vue 의 코드를 github에서 다운받아 필요한 부분에 로그를 추가하고 눈으로 확인할 수 있습니다. 전체 과정에 대한 화면 입니다.
269+
빨간색 사각형 안의 내용을 따라가면서 보시면 됩니다.
270+
271+
![Imgur](http://i.imgur.com/vvY8LRl.png)
272+
273+
![Imgur](http://i.imgur.com/rAvSCqB.png)
274+
275+
![Imgur](http://i.imgur.com/dB28Sz7.png)
276+
277+
![Imgur](http://i.imgur.com/rVXFRoI.png)
278+
279+
![Imgur](http://i.imgur.com/VkgRy9U.png)
280+
281+
![Imgur](http://i.imgur.com/cgkt450.png)
282+
283+
![Imgur](http://i.imgur.com/tKSJooc.png)
284+
285+
![Imgur](http://i.imgur.com/QLy2o9n.png)
286+
287+
![Imgur](http://i.imgur.com/Jg33100.png)
288+
289+
감사합니다.
290+

0 commit comments

Comments
 (0)