본문 바로가기

FrontEnd

Vue.js 내부 들여다보기[Demystifying Vue.js internals]

반응형

vue.js의 내부 구현을 알아봅니다.
원문 : https://medium.com/js-imaginea/the-vue-js-internals-7b76f76813e3

 

Demystifying Vue.js internals

When it comes to JavaScript frameworks, Vue.js is a trending UI framework.(just crossed 90k Github ⭐️ and more than 13k 🍴, very much…

medium.com

저는 github의 소스를 살펴보고 vue 내부에서 무슨 일이 일어나는지 이해하기 위해 여러 차례 디버깅을 했습니다.
이 게시물은 여기 4가지 질문에 대한 답변을 제공하려고 합니다.

  1. Vue.js 인스턴스를 생성하면 무슨 일이 일어날까요?
  2. 템플릿은 내부적으로 어떻게 처리되나요?
  3. 가상 DOM의 중요성은 무엇인가요?
  4. property가 변경될 때 템플릿 리렌더링은 어떻게 발생하나요?

컴포넌트는 브라우저에 표시하기 전에 다양한 단계를 통과해야 하는 템플릿을 포함합니다.
작은 템플릿을 예제로 사용하여 설명하겠습니다.

<div id="app">
  <span v-if="dynamic">Dynamic text</span>
  <span><p>Static text</p></span>
  <button @click="toggleFlag">Toggle Dynamic</button>
</div>
템플릿이 자명(토글)하기 때문에 컴포넌트의 JS 코드를 작성하지 않습니다.

컴파일 단계

Vue 컴파일러는 컴포넌트의 템플릿을 읽고 파싱, 최적화, 코드 생성과 같은 단계를 거쳐 궁극적으로 렌더링 함수를 만듭니다.
이 함수는 가상 DOM 패치 프로세스(Virtual DOM patch process)에서 실제 DOM을 생성하는 데 사용하는 VNode 생성을 담당합니다.

파싱 단계

이 컴파일 단계는 특정 컴포넌트의 템플릿으로 전달된 마크업과 함께 수행됩니다.
이미지에서 볼 수 있듯이 파서는 먼저 템플릿을 HTML 파서로 파싱하고
차례로 AST(추상 구문 트리;Abstract syntax tree)로 변환합니다.

AST &amp;mdash; After Parsing stage
AST는 attribute, parent, children, tag 등과 같은 정보를 포함합니다.
구문 분석(Parsing) 프로세스는 엘리먼트와 유사한 지시문(directive)을 구문 분석합니다.
v-for, v-if, v-once와 같은 구조적 지시문(structural directives)는 AST에서 특정 엘리먼트에 대한 키-값 쌍으로 표시됩니다.
템플릿의 v-if 지시문(directive)은 구문 분석(Parsing) 후 {v-if: "dynamic"}과 같은 객체로 attrsMap에 푸시됩니다.

최적화 단계

옵티마이저의 목표는 생성된 AST를 살펴보고 완전히 정적인 서브 트리,
즉 변경이 필요하지 않은 DOM 부분을 감지하는 것입니다.
이미지에 표시된 대로 이러한 요소는 정적(static)으로 표시됩니다.
AST &amp;mdash; After optimization

정적 하위 트리를 감지하면 Vue가 이를 상수로 호이스트하여 Vue가 다시 렌더링할 때마다 새로운 노드를 생성하지 않도록 합니다.
이러한 노드는 가상 DOM의 패치 프로세스(patching process) 중에 완전히 건너뜁니다.


코드 생성(CodeGen) 단계

컴파일러의 마지막 단계는 codegen 단계로 가상 돔의 패치 과정에서 사용할 실제 렌더 함수를 생성하는 단계입니다.

Hierarchy of render functions

위 이미지에서 템플릿 계층이 렌더 함수 계층으로 변환된 것을 볼 수 있습니다.
최적화 프로그램에서 제공하는 static 플래그를 기반으로 codegen은 렌더 함수를 2개의 개별 함수로 분기합니다.
하나는 단순(simple) 렌더 함수이고 다른 하나는 정적 렌더 함수입니다.
이 렌더링 함수는 실제 렌더링 프로세스를 트리거하는 동안 VNode를 생성하는 데 사용됩니다.

참고: 빌드 단계를 사용하는 경우 템플릿 컴파일이 미리 수행됩니다.
예를 들어 SFC(single file components)를 사용할 경우입니다.


반응형 컴포넌트의 Observer & Watcher

Observer

내부적으로 Vue는 데이터에 정의한 모든 속성을 살펴보고 Object.defineProperty를 사용하여
이를 getter/setter로 변환합니다.

주 : 요새는 프록시 쓰는걸로 압니다
var o = Object.create(null);
var bValue = 12;
Object.defineProperty(o, 'b', {
  get() {
    return bValue;
  },
  set(newValue) {
    bValue = newValue;
    /*
      In our example when the value of dynamic data property change it's watcher will be notify
    */
  } 
});
o.b = 45 // notify the watcher
데이터 property가 새 값을 가져오면 set 함수가 Watcher들에 알립니다.

Watcher

Vue 애플리케이션이 초기화될 때 각 컴포넌트에 대해 Watcher가 생성됩니다.
표현식을 구문 분석하고 Subscriber를 수집하며 표현식 값이 변경되면 콜백을 실행합니다.
이것은 $watch api 및 지시문(directives) 모두에 사용됩니다.
모든 컴포넌트 인스턴스에는 해당 Watcher 인스턴스가 있으며
컴포넌트는 렌더링 하는 동안 의존성을 watcher에 알립니다. (ex watchEffect)
나중에 의존성의 setter가 트리거되면 watcher는 궁극적으로 패치 프로세스를 트리거합니다. 
데이터 변경이 관찰될 때마다 watchers 큐를 열고 동일한 이벤트 루프에서 발생하는 모든 데이터 변경을 버퍼링합니다.
모든 watcher가 대기열에 추가됩니다.
각 watcher는 증분 순서의 고유한 Id(++id)를 갖습니다.
따라서, 동일한 watcher가 여러 번 트리거되면 큐에 한 번만 푸시되고
큐가 해당 watcher를 소비하기 전에 watcher가 상위에서 하위로 실행되어야 하므로, id 기준으로 큐를 정렬합니다.
 
내부적으로 Vue는 비동기 큐에 setTimeout(fn, 0), Promise.then, MessageChannel을 사용합니다.
 
nextTick 함수는 대기열 내의 모든 watcher를 소비하고 큐를 플러시합니다.
모든 watch가 소비되고 큐가 플러시되면 watcher의 run() 함수에서 렌더링 프로세스가 시작됩니다.

패치 프로세스(The Patch proces)

https://vuejs.org/guide/extras/rendering-mechanism.html
패치 프로세스는 가상 DOM을 사용하여 실제 DOM과 효율적으로 상호 작용하는 프로세스입니다.
가상 DOM은 DOM(문서 개체 모델)을 나타내는 JavaScript 객체입니다.
Vue.js는 내부적으로 snabbdom 라이브러리를 사용하고 있습니다.
이 패치 프로세스에 정확히 어떤 내용이 포함되는지 살펴보겠습니다.


이 프로세스는 모두 이전 VNode(가상 DOM 노드)와 새로운 VNode 에 관한 이야기 입니다.
즉, 이 둘을 서로 비교할 것입니다.

알고리즘은 다음과 같은 방식으로 동작합니다.

  1. 먼저 Old VNode가 있는지 여부를 확인하고 없으면 각 VNode에 대한 DOM 요소를 만듭니다.
    •  앱을 처음 구동하고 첫 번째 렌더링 프로세스가 시작된 경우, 이전 VNode가 없을 것입니다.
  2. 이전 VNode가 있는 경우 두 자식 비교 프로세스가 시작됩니다.
    • 공통 노드는 DOM에 유지됩니다.
    • 새 노드는 추가됩니다.
    • 일치하지 않는 노드는 Virtual DOM에서 제거된 것이며, 실제 돔에서 제거됩니다.

모든 노드에 대해 동일한 프로세스가 재귀적으로 수행됩니다.
또한, 최적화 단계에서 논의한 정적 노드를 떠올려 봅시다.
정적 노드의 트리는 그대로 사용됩니다.
우리는 이런 종류의 트리에 대해 실제 DOM과 상호 작용할 필요가 없습니다.


라이프 사이클 훅

특정 컴포넌트의 수명에 대해 논의해 보겠습니다.
컴포넌트의 생명주기는 4개의 섹션으로 구분할 수 있습니다.
  • The Creation
  • The Mounting
  • The Updating
  • The Destruction.
Vue의 새 인스턴스가 실행되자마자 컴포넌트 생성 프로세스가 시작됩니다.
  • beforeCreation: 이벤트를 수집하기 전, 컴포넌트에 필요한 데이터. 즉, watcher / 의존성을 수집하는 프로세스를 진행 중입니다.
  • created: Vue가 data와 watcher의 setup을 마쳤을 때의 상태입니다.
  • beforeMount: 패치 프로세스 실행 전 상태입니다. VNode는 data와 watcher를 기반으로 생성됩니다.
  • mount : 생성을 위한 패치 프로세스를 완료한 이후 상태를 나타냅니다.
  • beforeUpdate: watcher는 VNode를 업데이트하고 데이터가 변경되면 패치 프로세스를 다시 시작합니다.
  • update : 업데이트를 위한 패치 프로세스가 완료 된 상태입니다.
  • beforeDestory : 컴포넌트를 제거하기 이전 상태입니다. 컴포넌트는 화면에 나타난 상태이며 기능 중입니다.
  • destroyed : watcher를 제거하고, 해당 컴포넌트의 이벤트 리스너와 칠드런 컴포넌트들을 제거한 상태입니다.

결론

  • 컴포넌트 단위로 렌더링 함수가 생성되며 이때 정적 렌더 함수를 호이스팅하여 업데이트 계산에서 제외한다
  • 부모 컴포넌트의 리렌더링 반드시 자식 컴포넌트의 리렌더링을 유발하는 것은 아니며, reactivity 의존성에 따라 다르다.
  • 컴파일 단계는 정적 템플릿의 최적화를 해준다

더보기 : https://vuejs.org/guide/extras/rendering-mechanism.html

Originally posted at blog.imaginea.com

반응형