본문 바로가기

FrontEnd

Vue3 반응성 완벽 이해 3편 : vue3 core 코드 직접 읽어보기

반응형

Vue3의 창시자인 에반 유와 함께 Vue3 Reactivity 모듈 코드를 읽어봅니다.

이전 시리즈

2022.12.10 - [Vue.js] - Vue3의 반응성 완벽 이해 1편 : 종속성 추적

 

Vue3의 반응성 완벽 이해 1편 : 종속성 추적

vue mastery의 리액트 코어팀이 설명해주는 vue3의 반응성의 원리를 알아봅니다. 공식 문서와 nhn의 게시물을 읽어봤지만, 약간 납득이 안되는 부분들이 있었습니다. 해당 강의를 통해 어느정도 해소

itchallenger.tistory.com

2022.12.11 - [Vue.js] - Vue3의 반응성 완벽 이해 2편 : ref와 computed

 

Vue3의 반응성 완벽 이해 2편 : ref와 computed

Vue3의 반응성을 직접 구현하며 Vue3에 대한 더 깊은 이해를 도모해 봅시다. 이전 시리즈 : 2022.12.10 - [Vue.js] - Vue3의 반응성 완벽 이해 1편 : 종속성 추적 Vue3의 반응성 완벽 이해 1편 : 종속성 추적 vue

itchallenger.tistory.com

Basehandlers.ts

코드 주소 : https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts

 

GitHub - vuejs/core: 🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web. - GitHub - vuejs/core: 🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework fo...

github.com

핸들러는 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를 해당 인스턴스에 전달한다.

(뒷부분 추가 작성중)

반응형