
// ref.ts (단순화)
class RefImpl {
_value // ← 실제 값
_rawValue // ← 원본 값 (비교용)
dep = undefined // ← 이 ref를 구독 중인 effect 집합
constructor(value) {
this._rawValue = value
this._value = value // (객체면 reactive()로 감싸지만 일단 생략)
}
get value() {
trackRefValue(this) // ← "지금 누가 나를 읽고 있으면 dep에 등록해줘"
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) { // ← 값이 실제로 바뀌었을 때만
this._rawValue = newVal
this._value = newVal
triggerRefValue(this) // ← "나를 구독 중인 effect들에게 알려줘"
}
}
}// ref를 구독중이라면 dep에 등록
function trackRefValue(ref) {
// activeEffect: 지금 실행 중인 effect (computed, watch, 렌더 함수 등)
if (activeEffect) {
ref.dep = ref.dep || new Set()
ref.dep.add(activeEffect) // ← "현재 effect가 이 ref를 구독 중"이라고 기록
}
}
// ref를 구독중인 이펙트들에게 전파
function triggerRefValue(ref) {
if (ref.dep) {
ref.dep.forEach(effect => {
if (effect.scheduler) {
effect.scheduler() // ← 렌더 effect는 이 경로 (바로 실행 말고 큐에 넣음)
} else {
effect.run() // ← 일반 effect는 바로 실행
}
})
}
}// renderer.ts (단순화)
const setupRenderEffect = (instance, container) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 최초 마운트: 렌더 함수 실행 → DOM 생성
const subTree = renderComponentRoot(instance)
patch(null, subTree, container)
instance.isMounted = true
} else {
// 업데이트: 렌더 함수 다시 실행 → 이전 vnode와 비교 → DOM 갱신
const nextTree = renderComponentRoot(instance)
patch(instance.subTree, nextTree, container)
instance.subTree = nextTree
}
}
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update) // ← scheduler: 바로 실행 말고 큐에 넣어라
)
effect.run() // ← 최초 실행 (마운트) + 의존성 수집
}effect.run() 호출
→ activeEffect = 렌더 effect
→ renderComponentRoot() 실행
→ template 안에서 count.value 읽기
→ trackRefValue(count)
→ count.dep.add(렌더 effect) ← 이 컴포넌트가 count를 구독 중으로 등록
→ activeEffect 복원count.value = 1 (기존: 0)
→ hasChanged(1, 0) === true
→ triggerRefValue(count)
→ count.dep 순회
→ 렌더 effect 발견
→ effect.scheduler() 호출
→ queueJob(instance.update) ← "큐에 등록"count.value = 1
name.value = 'kim'
flag.value = true// scheduler.ts (단순화)
const queue = []
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
}
// 아직 실행 예약 안 됐으면: 마이크로태스크로 예약
if (!isFlushPending) {
isFlushPending = true
Promise.resolve().then(flushJobs) // ← 동기 코드가 전부 끝난 직후 실행
}
}[동기 코드 실행]
count.value = 1 → 값 즉시 변경 완료 → "렌더링 job" 큐에 추가, 마이크로태스크 예약
name.value = 'kim' → 값 즉시 변경 완료 → "렌더링 job" 이미 큐에 있음 → skip
flag.value = true → 값 즉시 변경 완료 → "렌더링 job" 이미 큐에 있음 → skip
// 동기 코드 종료: 값은 셋 다 이미 바뀐 상태
[마이크로태스크 실행]
→ flushJobs()
→ instance.update() 한 번만 실행
→ count=1, name='kim', flag=true 를 한 번에 화면에 반영count.value = 1
console.log(el.textContent) // ← 아직 이전 값 (마이크로태스크 실행 전)
await nextTick() // ← 마이크로태스크(flushJobs)가 끝날 때까지 기다림
console.log(el.textContent) // ← 새 값 반영됨export function nextTick(fn) {
const p = currentFlushPromise || Promise.resolve()
return fn ? p.then(fn) : p
// currentFlushPromise: 현재 진행 중인 flushJobs Promise
// → flushJobs가 끝난 뒤에 fn을 실행
}const componentUpdateFn = () => {
const nextTree = renderComponentRoot(instance) // ← 새 vnode 생성
const prevTree = instance.subTree // ← 이전 vnode
instance.subTree = nextTree
patch(prevTree, nextTree, container) // ← 비교 후 DOM 업데이트
}
<div>
<h1>{{ title }}</h1>
<p :class="{ active: isActive }">{{ count }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>patch(oldRoot, newRoot)
└─ div 타입 동일 → patchElement
├─ h1: 텍스트 동일 → 아무것도 안 함
├─ p: 타입 동일 → patchElement
│ ├─ patchProps: class 동일 → 아무것도 안 함
│ └─ patchChildren: 텍스트 '0' → '1'
│ → el.textContent = '1' ← DOM 조작 1회
└─ ul: 자식 동일 → 아무것도 안 함patch(oldRoot, newRoot)
└─ div → patchElement
├─ h1: 동일 → skip
├─ p → patchElement
│ ├─ patchProps: class "" → "active"
│ │ → el.classList.add('active') ← DOM 조작 1회
│ └─ patchChildren: '0' → '1'
│ → el.textContent = '1' ← DOM 조작 1회
└─ ul: 동일 → skip이전 리스트: [A, B, C] (key: 1, 2, 3)
새 리스트: [A, X, B, C] (key: 1, 4, 2, 3)
key 비교:
A(1): 위치 동일 → DOM 이동 없음
X(4): 이전에 없음 → el 새로 생성 후 B 앞에 insertBefore ← DOM 조작 1회
B(2): 위치 뒤로 밀림 → 이미 올바른 위치 (X가 삽입되면서)
C(3): 동일 → 변경 없음oldVnode: { type: 'p', children: '내용' }
newVnode: { type: 'div', children: '내용' }
→ 타입 불일치 → unmount(p 엘리먼트) ← 기존 DOM 제거
→ mount(div 엘리먼트) ← 새 DOM 생성 및 삽입patch(oldVnode, newVnode)
├─ 타입이 다르면 → 기존 DOM 제거 + 새 DOM 생성
└─ 타입이 같으면
├─ 속성(class, style, 이벤트 등) 비교 → 달라진 것만 업데이트
└─ 자식 비교
├─ 텍스트면 → el.textContent = 새 값
├─ 배열(리스트)이면 → key 기반으로 최소 변경 계산
└─ 없어진 자식 → DOM에서 제거
ref | reactive | computed | |
내부 구조 | RefImpl (.value 접근) | Proxy (프로퍼티 직접 접근) | ComputedRefImpl (_dirty 플래그) |
의존성 저장 | ref.dep (자체 보유) | targetMap[target][key] | effect.deps |
변경 감지 | .value setter | Proxy set 트랩 | 의존성 변경 시 _dirty = true |
렌더링 연결 | 동일: queueJob → flushJobs → patch | 동일 | 동일 |
const isVisible = ref(false)
const showAndMeasure = async () => {
isVisible.value = true // ← 큐에 등록됨, DOM은 아직 안 바뀜
const height = el.value?.offsetHeight // ← 0 또는 이전 값 (DOM 미반영 상태)
await nextTick() // ← 마이크로태스크 flush 대기
const height = el.value?.offsetHeight // ← 올바른 값
}const updateAll = () => {
userInfo.value.name = 'kim' // → queueJob
userInfo.value.age = 30 // → 이미 큐에 있으니 skip
userInfo.value.role = 'admin' // → skip
}
// → 렌더링은 1회만 발생ref 변경
→ 렌더 effect → queueJob (렌더링 큐)
→ watchEffect → queuePostFlushCb (렌더링 후 큐)
flushJobs 실행
→ 렌더링 완료
→ postFlush 실행 → watchEffect 콜백 ← DOM이 이미 업데이트된 상태