|
| 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 | + |
| 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 | + |
| 272 | +
|
| 273 | + |
| 274 | +
|
| 275 | + |
| 276 | +
|
| 277 | + |
| 278 | +
|
| 279 | + |
| 280 | +
|
| 281 | + |
| 282 | +
|
| 283 | + |
| 284 | +
|
| 285 | + |
| 286 | +
|
| 287 | + |
| 288 | +
|
| 289 | +감사합니다. |
| 290 | +
|
0 commit comments