Vue3의 창시자인 에반 유와 함께 Vue3 Reactivity 모듈 코드를 읽어봅니다.
이전 시리즈
2022.12.10 - [Vue.js] - Vue3의 반응성 완벽 이해 1편 : 종속성 추적
2022.12.11 - [Vue.js] - Vue3의 반응성 완벽 이해 2편 : ref와 computed
Basehandlers.ts
코드 주소 : https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts
핸들러는 reactive 내에서 생성되는 Proxy의 핸들러 인수를 의미합니다.
즉, 각 (인스턴스)메서드는 프록시 트랩 입니다.
해당 모듈에서 가장 중요한 함수는 createGetter, createSetter 두 함수입니다.
그 전에 기술적으로 알아둘 내용이 있습니다.
Vue3은 중첩된 객체에도 반응성을 제공합니다.
하지만 프록시를 사용하기 때문에, 대상 객체 내부에 외부 객체가 존재할 경우, 동등 비교 시 문제가 생깁니다.
Vue3는 내부적으로 프록시와의 동등비교를 기존 객체와의 비교로 변경합니다.(해당 라인으로 이동)
이는 반응성 참조 객체와 실제 객체가 별도로 존재할 수 있다는 것에서 오는 문제인데요.
이와 관련하여 추가로 hasOwnProperty를 사용하면 항상 프록시가 기존 객체를 참조할 수 있도록 트랩 함수를 제공합니다.
// js 네이티브 메서드 호출을 프록시
function hasOwnProperty(key: string) {
// @ts-ignore
const obj = toRaw(this)
track(obj, TrackOpTypes.HAS, key)
return obj.hasOwnProperty(key)
}
createGetter
해당 코드에서 주목할 부분은 솔직히 하나입니다.
- 중첩 객체들도 재귀적으로 반응성을 제공한다
- 반응성 제공은 트랩 메서드가 호출될 때에 lazy하게 일어난다
중요하다 생각한 부분들을 주석으로 처리했습니다만,
유틸리티 메서드의 구현을 직접 확인하시면서 읽어보시는 것을 추천드립니다.
해당 라인으로 이동 : https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts#L55
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// Vue3 내부적으로 사용. trap이 호출되는 경우
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
// 배열일 경우 다른 방식으로 동작함
const targetIsArray = isArray(target)
// 읽기 전용이 아닐 때, 배열은 이전의 예외처리가 추가된 핸들러를 이용하도록 처리함
// hasOwnProperty의 경우도 예외처리
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 일단 Reflect를 통해 target의 메서드를 호출
const res = Reflect.get(target, key, receiver)
// ES6 심볼 관련 예외 처리, __proto__등. 추적하지 않음.
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
// 메서드 호출 결과 리턴
return res
}
// readOnly가 아닌 경우 해당 객체 추적
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// shallow인 경우 내부 반응성을 제공하지 않음. 메서드 호출 결과 리턴
if (shallow) {
return res
}
// 메서드 호출 결과가 반응성 객체인 경우
// 객체 내에 반응성 객체를 넣어 초기화 한 경우
// target이 배열이고 숫자 인덱싱하여 검색하는 경우 반응형 객체를 그대로 리턴함
// 그 외에는 value를 리턴함.
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// 호출 결과가 객체인 경우 재귀적으로 리액티브하게 해줌
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
createSetter
해당 메서드의 중요한 두 가지 역할은 다음과 같습니다.
- set 메서드가 호출되었을 때, 기존 값을 신규 값으로 대체
- oldValue가 readOnly인 객체인데, 해당 객체의 value에 참조가 아닌 값을 할당하려는 경우 실패(false)
- shallow가 아닐 경우 (shallow면 처리 x)
- 신규 값이 ReadOnly가 아니면서 Shallow가 아닐 경우 raw 값으로 변경
- target의 속성일 경우는 처리함
- target[key]가 객체일 경우, 해당 객체의 프록시가 처리하도록 위임
- set 메서드가 호출되었을 때, 적절한 effect를 트리거
- 키가 없었는데 생겼을 경우(ADD)
- 키가 바뀌었을 경우 (SET)
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Vue3은 어떻게 리렌더링할 컴포넌트를 알까?
component.ts를 읽어보면, 인스턴스 내부에 effect 플래그를 이용해 현재 처리중인 effect를 인스턴스 필드에 등록하는 것을 알 수 있다.
rerenderer.ts에서 setup이 왼료된 이후에 update effect를 해당 인스턴스에 전달한다.
(뒷부분 추가 작성중)
'FrontEnd' 카테고리의 다른 글
[Vue3] props와 컴포넌트 상태 동기화 (0) | 2022.12.16 |
---|---|
Vue3가 DOM 업데이트를 비동기적으로 수행하는 방법 (0) | 2022.12.14 |
Vue3의 반응성 완벽 이해 2편 : ref와 computed (0) | 2022.12.11 |
Vue3의 반응성 완벽 이해 1편 : 종속성 추적 (4) | 2022.12.10 |
zustand와 react query를 같이 사용하는 방법 (1) | 2022.12.09 |