FrameWorks/Vue

Vue의 nextTick() 파헤치기

ABCD 2024. 6. 12.

nextTick( )

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p;
}

nextTick( )

  • 다음 DOM 업데이트 주기가 끝난 후 실행할 지연된 콜백
  • 데이터를 수정한 직후에 이 방법을 사용하여 업데이트 된 DOM을 가져온다.
const message = ref('Hello!')
const changeMessage = async newMessage => {
  message.value = newMessage
  // 여기서 DOM에서 얻은 값은 이전 값입니다.
  await nextTick()
    // nextTick이 업데이트된 값이 된 후 DOM 값을 가져옵니다.
  console.log('Now DOM is updated')
}
const count = ref(0)

async function increment() {
  count.value++

  // DOM not yet updated
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM is now updated
  console.log(document.getElementById('counter').textContent) // 1
}

//위 코드와 아래 코드는 동일하게 작동한다.
const count = ref(0)
function increment() {
  count.value++

  // DOM not yet updated
  console.log(document.getElementById('counter').textContent) // 0

  // DOM is now updated
  nextTick(()=>{

  console.log(document.getElementById('counter').textContent) // 1
  })
}

JS 실행 매커니즘

  • JS는 단일 쓰레드 언어이다. 즉, 한 번에 한 가지 작업만 수행할 수 있다.
    • 멀티 쓰레드는 동시에 여러작업을 수행할 수 있다.
  • 단일 쓰레드는 모든 작업이 대기열(큐)에 있어야 하며, 후속작업은 이전작업이 끝날 때 까지 기다려야 한다.
  • 이전 작업이 오래 걸리면 사용자 입 장에서 대기할 필요가 없는 일부 작업은 영원히 대기하게 된다.
    • 이 어이없는 문제를 해결하기 위해 JS에서 비동기라는 개념이 탄생했다.

개념

  • 기본 스레드에서 실행을 위해 대기 중인 작업을 동기화한다. 이전 작업 완료시 다음 작업을 실행한다.
  • 비동기 작업은 ‘태스크 큐’에 들어간다.
  • 태스크 큐가 메인 쓰레드에 비동기 태스크를 실행할 수 있음을 알리는 경우에만 메인 쓰레드에 들어간다.

동작 매커니즘

  • 모든 동기화 작업은 메인 쓰레드에서 실행되며, 실행 컨텍스트 스택을 형성한다.
  • 비동기 작업의 실행 결과가 있따면 해당 이벤트는 태스크 큐에 배치된다.
  • 실행 컨텍스트 스택의 모든 동기작업이 실행된 후 시스템은 태스크 큐에 어떤 이벤트가 있는지 확인한다.
    • 해당 비동기 작업은 대기 상태를 종료하고 실행 컨텍스트 스택에 들어가 실행을 시작한다.
  • 메인 쓰레드는 위 세 단계를 계속해서 반복한다.

Event Loop

nextTick 구현

  • 위의 개념을 기반으로 작업을 비동기화 하기 위한 수단이다.
const p = Promise.resolve();
export function nextTick(fn?: () => void): Promise<void> {
  return fn ? p.then(fn) : p;
}

실행 순서는 어떻게 보장하지??

  • Vue3의 Scheduler 모듈은 작업 Queue와 콜백 Queue를 별도로 유지한다.
    • queueJob
      • 작업 큐잉
      • 작업 중복 제거
      • 태스크의 고유성을 보장하기 위해 각 작업 큐잉 시에 queueFlush호출
    • queuePostFlushCb
      • 콜백 큐잉
      • 콜백 중복 제거
      • 각 콜백 큐잉 시 callqueueFlush 실행
  • 해당 큐들을 별도로 유지하는 이유는 실행 시점 때문이다.
const queue: (Job | null)[] =[];
export function queueJob(job: Job) {
  if(!queue.inclues(job)) {
    queue.push(job);
    queueFlush();
  }
}

export function queuePostFlushCb(cb: Function | Function[]) {
  if(!isArray(cb)) {
    postFlushCbs.push(cb);
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush();
}

queueFlush( )

  • nextTick 함수를 통해 flushJob 함수를 비동기적으로 수행한다.
function queueFlush() {
  if(!isFlushing && !isFlushPending) {
    isFlushPending = true;
    nextTick(flushJobs);
  }
}

flushJob( )

  • 가장 먼저 작업 queue를 정렬 한 후 처리한다.

    • flush 전에 대기열을 정렬한다. 이 때 다음을 보장한다.

      • 컴포넌트는 부모에서 자식으로 업데이트된다.

        → 부모는 항상 자식보다 먼저 생성되므로 렌더 이펙트의 우선순위가 높기때문

      • 상위 컴포넌트 중 컴포넌트가 마운트 해제된 경우 업데이트를 skip할 수 있다.

        → 작업은 다른 flush된 작업이 실행되는 동안 무효화되기만 하며, flush시작 전 null이 될 수 없다.

  • 그 후 지연 콜백 큐인 postFlushCbs를 처리한다.

    • postFlushCbs 처리가 작업을 트리거 할 수 있다.
    • 재귀적으로 같은 작업을 반복한다.
function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {
    seen = seen || new Map()
  }

  queue.sort(comparator)

  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        // console.log(`running:`, job.id)
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0
    queue.length = 0

    //postFlushCbs를 처리하는 곳
    flushPostFlushCbs(seen)

    isFlushing = false
    currentFlushPromise = null
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}
728x90
반응형

'FrameWorks > Vue' 카테고리의 다른 글

Vue의 watchEffect 와 watch 파헤치기  (0) 2024.06.12
Vue의 ModelValue 파헤치기  (0) 2024.06.12
Vue의 BaseHandler 파헤치기  (0) 2024.06.12
Vue의 ref 파헤치기  (0) 2024.06.12
reactive( )  (0) 2023.07.11

댓글

💲 추천 글