blog

vue 킵-얼라이브 컴포넌트 원리

keep-alive는 vue에서 제공하는 추상 컴포넌트로, 래핑된 하위 컴포넌트를 캐시하여 리소스 소비를 줄이며, 이 글에서는 주로 vue에서 keep-alive 컴포넌트를 구현하...

Oct 16, 2025 · 21 min. read
シェア

Vue 킵-얼라이브 컴포넌트 원칙

컴포넌트 구현 원칙

// src/core/components/keep-alive.js
export default {
 name: 'keep-alive',
 abstract: true,
 props: {
 include: patternTypes,
 exclude: patternTypes,
 max: [String, Number]
 },
 methods: {
 cacheVNode() {
 const { cache, keys, vnodeToCache, keyToCache } = this
 if (vnodeToCache) {
 const { tag, componentInstance, componentOptions } = vnodeToCache
 cache[keyToCache] = {
 name: getComponentName(componentOptions),
 tag,
 componentInstance,
 }
 keys.push(keyToCache)
 // prune oldest entry
 if (this.max && keys.length > parseInt(this.max)) {
 pruneCacheEntry(cache, keys[0], keys, this._vnode)
 }
 this.vnodeToCache = null
 }
 }
 },
 created () {
 this.cache = Object.create(null)
 this.keys = []
 },
 destroyed () {
 for (const key in this.cache) {
 pruneCacheEntry(this.cache, key, this.keys)
 }
 },
 mounted () {
 this.cacheVNode()
 this.$watch('include', val => {
 pruneCache(this, name => matches(val, name))
 })
 this.$watch('exclude', val => {
 pruneCache(this, name => !matches(val, name))
 })
 },
 updated () {
 this.cacheVNode()
 },
 render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
 // check pattern
 const name: ?string = getComponentName(componentOptions)
 const { include, exclude } = this
 if (
 // not included
 (include && (!name || !matches(include, name))) ||
 // excluded
 (exclude && name && matches(exclude, name))
 ) {
 return vnode
 }
 const { cache, keys } = this
 const key: ?string = vnode.key == null
 // 동일한 생성자가 다른 로컬 컴포넌트로 등록될 수 있으므로 cid만으로는 충분하지 않습니다.
 ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
 : vnode.key
 if (cache[key]) {
 // 하위 컴포넌트가 이미 캐시된 경우 컴포넌트의 인스턴스를 가져옵니다.
 vnode.componentInstance = cache[key].componentInstance
 // 캐시된 키 업데이트
 remove(keys, key)
 keys.push(key)
 } else {
 // 캐시는 노드와 노드의 키 값을 캐시합니다.
 this.vnodeToCache = vnode
 this.keyToCache = key
 }
 // 캐시된 노드에 keepAlive 속성을 true로 추가합니다.
 vnode.data.keepAlive = true
 }
 return vnode || (slot && slot[0])
 }
}

컴포넌트 원리

는 래핑된 자식 컴포넌트에 대한 처리만 수행하는 추상 컴포넌트이며, DOM 요소 자체를 렌더링하지도 않고 부모 컴포넌트의 체인에 나타나지도 않습니다. 그렇다면 추상 컴포넌트는 어떻게 구현할 수 있을까요?

컴포넌트는 값이 true인 추상 속성을 가지며, 래핑된 하위 컴포넌트는 initLifcycle 메서드를 실행할 때 부모 컴포넌트가 추상 컴포넌트인지 확인하고, 추상 컴포넌트인 경우 이 컴포넌트와 부모-자식 관계를 무시하고 상위 레벨 컴포넌트를 부모로 설정합니다.

// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
 const options = vm.$options
 let parent = options.parent
 if (parent && !options.abstract) {
 while (parent.$options.abstract && parent.$parent) {
 parent = parent.$parent
 }
 parent.$children.push(vm)
 }
 vm.$parent = parent
 vm.$root = parent ? parent.$root : vm
 // ...
}

컴포넌트 템플릿을 작성하는 대신 컴포넌트는 렌더링 메서드를 구현합니다. 컴포넌트를 렌더링할 때 자체 렌더링 메서드를 호출하고 렌더링 결과는 렌더링 메서드의 반환 결과에 따라 결정됩니다.

// src/core/components/keep-alive.js
render () {
 // 기본 슬롯 가져오기
 const slot = this.$slots.default
 // 슬롯에서 첫 번째 노드 가져오기
 const vnode: VNode = getFirstComponentChild(slot)
 // 컴포넌트 옵션 가져오기
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
 // 컴포넌트 이름 가져오기
 const name: ?string = getComponentName(componentOptions)
 // 노드가 캐시되었는지 확인
 const { include, exclude } = this
 // 캐싱 없이
 if (
 // not included
 (include && (!name || !matches(include, name))) ||
 // excluded
 (exclude && name && matches(exclude, name))
 ) {
 return vnode
 }
 const { cache, keys } = this
 // 컴포넌트의 키 가져오기
 const key: ?string = vnode.key == null
 // 동일한 생성자가 다른 로컬 컴포넌트로 등록될 수 있으므로 cid만으로는 충분하지 않습니다.
 ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
 : vnode.key
 if (cache[key]) {
 // 컴포넌트 노드가 이미 캐시된 경우 컴포넌트의 인스턴스를 가져옵니다.
 vnode.componentInstance = cache[key].componentInstance
 // 캐시된 키 업데이트
 remove(keys, key)
 keys.push(key)
 } else {
 // 캐시는 노드와 노드의 키 값을 캐시합니다.
 this.vnodeToCache = vnode
 this.keyToCache = key
 }
 // 캐시된 노드에 keepAlive 속성을 true로 추가합니다.
 vnode.data.keepAlive = true
 }
 return vnode || (slot && slot[0])
 }

렌더 메서드에서는 컴포넌트의 기본 슬롯을 가져온 다음 getFirstComponentChild 메서드를 호출하여 기본 슬롯에 있는 첫 번째 자식 요소의 vnode를 가져오고, 이는 렌더 메서드 실행 결과로 반환됩니다. 따라서 컴포넌트가 둘 이상의 요소를 래핑하는 경우 첫 번째 요소만 렌더링됩니다.

// 기본 슬롯 가져오기
const slot = this.$slots.default
// 슬롯에서 첫 번째 엘리먼트 노드 가져오기
const vnode: VNode = getFirstComponentChild(slot)

다음으로 getComponentName을 호출하여 컴포넌트의 이름과 컴포넌트가 수신한 포함 및 제외 매개변수를 가져옵니다. 컴포넌트 이름은 포함 및 제외 목록에 컴포넌트가 존재하는지 여부를 판단하여 캐시할 필요가 있는지 여부를 결정하는 데 사용됩니다. 컴포넌트를 캐시할 필요가 없는 경우 노드가 직접 반환되고, 그렇지 않은 경우 후속 처리가 수행됩니다.

const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } // match function matches (pattern: string | RegExp | Array<string>, name: string): boolean { if (Array.isArray(pattern)) { return pattern.indexOf(name) > -1 } else if (typeof pattern === 'string') { return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { return pattern.test(name) } /* istanbul ignore next */ return false }

컴포넌트는 생성된 후크 함수에서 캐시 및 키라는 두 가지 어트리뷰트를 정의합니다. 캐시에는 캐시할 컴포넌트 노드의 vnode가, 키에는 컴포넌트 노드의 키가 저장됩니다.

// src/core/components/keep-alive.js
created () {
 this.cache = Object.create(null)
 this.keys = []
 }

그런 다음 노드가 캐시에 존재하는지 여부를 판단하여 노드가 이미 캐시에 존재하는 경우, 즉 캐시에 적중하면 캐시된 노드의 인스턴스를 vnode.componentInstance에 할당하고 동시에 키 목록에 저장된 구성 요소 노드의 키를 업데이트하고, 캐시 적중이 없는 경우 캐시해야 할 구성 요소 노드와 해당 노드의 키를 vnodeToCache 및 keyToCache에 저장하고 동시에 vnode.data.keepAlive = true로 설정하여 노드를 캐시된 것으로 마킹합니다. vnodeToCache와 keyToCache에 저장하고 동시에 vnode.data.keepAlive = true, 즉 노드를 캐시된 노드로 표시한 다음 마지막으로 노드를 반환합니다.

const { cache, keys } = this
const key: ?string = vnode.key == null
 // 동일한 생성자가 다른 로컬 컴포넌트로 등록될 수 있으므로 cid만으로는 충분하지 않습니다.
 ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
 : vnode.key
if (cache[key]) {
 // 하위 컴포넌트가 이미 캐시된 경우 컴포넌트의 인스턴스를 가져옵니다.
 vnode.componentInstance = cache[key].componentInstance
 // 캐시된 키 업데이트
 remove(keys, key)
 keys.push(key)
} else {
 // 캐시는 노드와 노드의 키 값을 캐시합니다.
 this.vnodeToCache = vnode
 this.keyToCache = key
}
// 캐시된 노드에 keepAlive 속성을 true로 추가합니다.
vnode.data.keepAlive = true

컴포넌트 마운팅이 완료되면 컴포넌트의 마운트된 후크 함수가 실행되며, 이때 노드의 인스턴스가 vnode.componentInstance에 저장됩니다. cacheVnode 메서드를 호출하면 렌더 메서드에 캐시된 노드와 키가 캐시에 캐시됩니다. 또한 마운트된 노드와 키를 적시에 캐시 및 키에 업데이트하기 위해 포함 및 제외 리스너 메서드를 설정합니다. 최대값이 설정되고 키 목록의 길이가 최대값을 초과하면 pruneCacheEntry 메서드가 선입선출 방식으로 호출되어 캐시에서 가장 오랫동안 사용되지 않은 노드와 키를 검색하여 캐시에서 캐시된 노드를 가져옵니다. 노드가 캐시 및 키에서 제거되고 $destroy가 실행되어 컴포넌트 인스턴스가 소멸됩니다.

// src/core/components/keep-alive.js
mounted () {
 // 캐시된 노드
 this.cacheVNode()
 // 데이터 변경 사항을 포함하기 위한 수신 대기
 this.$watch('include', val => {
 pruneCache(this, name => matches(val, name))
 })
 // 데이터 변경 제외 수신 대기
 this.$watch('exclude', val => {
 pruneCache(this, name => !matches(val, name))
 })
 }
 
 methods: {
 cacheVNode() {
 const { cache, keys, vnodeToCache, keyToCache } = this
 if (vnodeToCache) {
 // 캐시는 동시에 여러 캐싱 작업을 방지합니다.
 const { tag, componentInstance, componentOptions } = vnodeToCache
 // 컴포넌트 저장 예시
 cache[keyToCache] = {
 name: getComponentName(componentOptions),
 tag,
 // 컴포넌트 인스턴스가 렌더링된 후에는$el 나중에 직접 재사용되는 어트리뷰트$el  
 componentInstance,
 }
 // 캐시된 컴포넌트의 키
 keys.push(keyToCache)
 // 캐시 업데이트 알고리즘은 최대 길이를 초과하면 첫 번째로 캐시된 컴포넌트와 해당 키가 제거됩니다.
 if (this.max && keys.length > parseInt(this.max)) {
 pruneCacheEntry(cache, keys[0], keys, this._vnode)
 }
 this.vnodeToCache = null
 }
 }
}
 
function pruneCacheEntry (
 cache: CacheEntryMap,
 key: string,
 keys: Array<string>,
 current?: VNode
) {
 const entry: ?CacheEntry = cache[key]
 if (entry && (!current || entry.tag !== current.tag)) {
 entry.componentInstance.$destroy()
 }
 cache[key] = null
 remove(keys, key)
}

컴포넌트 렌더링 프로세스

예시:

렌더링 초기화하기

Vue의 렌더링은 노드를 인터페이스에 마운트하기 위해 createElm 메서드가 실행되는 패치 프로세스를 거칩니다. createElm 메서드에서 createComponent 메서드가 실행되어 컴포넌트를 마운트합니다.

// src/vore/vdom/patch.js
function createElm (
 vnode,
 insertedVnodeQueue,
 parentElm,
 refElm,
 nested,
 ownerArray,
 index
 ) {
 if (isDef(vnode.elm) && isDef(ownerArray)) {
 /**
 * 이 가상 노드가 렌더링에 사용되었습니다.
 * 이 노드는 현재 새 노드로 사용되며 삽입 참조 노드로 사용될 때 엘름을 재정의하면 잠재적인 패치 오류가 발생할 수 있습니다. 대신 노드에 대한 연결을 생성하는 DOM 요소에서 필요에 따라 노드가 복제됩니다.
 */
 vnode = ownerArray[index] = cloneVNode(vnode)
 }
 vnode.isRootInsert = !nested // for transition enter check
 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
 return
 }
 // ...
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
 /**
 * vnode 가져오기.data  
 */
 let i = vnode.data
 if (isDef(i)) {
 /**
 * 컴포넌트 인스턴스가 이미 존재하는지 확인&& 킵-얼라이브에 래핑
 */
 const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
 /**
 * vnode 실행.data.hook.init 후크 함수는 컴포넌트가 사용할 수 있도록 렌더링 헬퍼가 됩니다.
 * 컴포넌트가 킵-얼라이브 래핑된 경우, 프리패치 훅이 실행되어 vnode의 프로퍼티로 oldVnode를 업데이트합니다.
 * 컴포넌트가 킵얼라이브 래핑되지 않았거나 처음으로 렌더링되는 경우, 컴포넌트는 초기화되고 마운트 단계로 들어갑니다.
 */
 if (isDef(i = i.hook) && isDef(i = i.init)) {
 i(vnode, false /* hydrating */)
 }
 
 if (isDef(vnode.componentInstance)) {
 /**
 * vnode가 하위 컴포넌트인 경우, init 훅이 호출되어 컴포넌트 인스턴스를 생성하고
 * 이 시점에서 컴포넌트에 각 모듈의 생성 훅을 실행하도록 지정할 수 있습니다.
 */
 
 initComponent(vnode, insertedVnodeQueue)
 /**
 * 컴포넌트의 DOM 노드를 부모 노드에 삽입하기
 */
 insert(parentElm, vnode.elm, refElm)
 if (isTrue(isReactivated)) {
 /**
 * 컴포넌트가 킵-얼라이브로 래핑될 때 활성화하기
 */
 reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
 }
 return true
 }
 }
 }

createComponent 메서드는 노드가 해당 시점에 활성화된 상태인지 여부를 표시하기 위해 isReactivated를 정의합니다. 컴포넌트가 처음 렌더링될 때는 컴포넌트의 렌더 메서드가 일찍 실행되고 이때 AC 컴포넌트가 인스턴스화되지 않았으므로 vnode.componentInstance가 null이고 따라서 현재 isReactivated가 거짓입니다. AC 컴포넌트 노드가 인스턴스화되면 componentInstance는 컴포넌트 인스턴스를 저장합니다. AC 컴포넌트가 다시 활성화되면 isReactivated는 참입니다.

const isReactivated = isDef(vnode.componentInstance) && i.keepAlive

다음으로 init 메서드를 실행하고, init 메서드에서 컴포넌트가 캐시되었는지 여부를 확인하고, 캐시된 경우 prepatch 메서드를 실행하고, 그렇지 않으면 createComponentInstanceForVnode 메서드를 호출하여 컴포넌트 인스턴스를 생성하고 vnode.componentInstance에 컴포넌트 인스턴스를 할당합니다.

// src/core/vdom/create-component.js
const componentVNodeHooks = {
 /**
 *  
 * @param {*} vnode 
 * @param {*} hydrating 
 */
 init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
 if (
 vnode.componentInstance &&
 !vnode.componentInstance._isDestroyed &&
 vnode.data.keepAlive
 ) {
 // keep-alive components, treat as a patch
 /**
 * 킵-얼라이브로 래핑된 컴포넌트
 */
 const mountedNode: any = vnode // work around flow
 componentVNodeHooks.prepatch(mountedNode, mountedNode)
 } else {
 /**
 * 컴포넌트 인스턴스, 즉 "새" vnode 생성하기.componentOptions.Ctor(options) => vue 컴포넌트 예제 가져오기
 */
 const child = vnode.componentInstance = createComponentInstanceForVnode(
 vnode,
 activeInstance
 )
 /**
 * 컴포넌트의$mount 다음 단계는 컴파일러에서 렌더링 함수를 가져온 다음 컴포넌트가 인터페이스에 렌더링될 때까지 마운팅 및 패치 경로를 따라가는 것입니다.
 */
 child.$mount(hydrating ? vnode.elm : undefined, hydrating)
 }
 },
 //...
}
// src/core/vdom/create-component.js
// 하위 컴포넌트 인스턴스 생성
export function createComponentInstanceForVnode (
 // we know it's MountedComponentVNode but flow doesn't
 vnode: any,
 // activeInstance in lifecycle state
 parent: any
): Component {
 const options: InternalComponentOptions = {
 _isComponent: true,
 _parentVnode: vnode,
 parent
 }
 /**
 * 인라인 템플릿 렌더링 함수 확인
 */
 const inlineTemplate = vnode.data.inlineTemplate
 if (isDef(inlineTemplate)) {
 options.render = inlineTemplate.render
 options.staticRenderFns = inlineTemplate.staticRenderFns
 }
 /**
 * new vnode.componentOptions.Ctor(options) => Vue  
 */
 return new vnode.componentOptions.Ctor(options)
}

컴포넌트 인스턴스화가 완료되면 렌더 컴파일 및 패치 마운팅을 위해 $mount 메서드가 실행됩니다. 마운트 메서드 실행이 완료되면 vnode.$el은 인터페이스에 마운트해야 하는 dom 요소를 저장합니다.

child.$mount(hydrating ? vnode.elm : undefined, hydrating)

마운트 메서드가 실행된 후에는 createComponent 메서드로 돌아가 후속 코드를 실행합니다. 이 시점에서 vnode.componentInstance는 컴포넌트 인스턴스와 $el을 저장했습니다.

if (isDef(vnode.componentInstance)) {
 initComponent(vnode, insertedVnodeQueue)
 insert(parentElm, vnode.elm, refElm)
 if (isTrue(isReactivated)) {
 reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
 }
 return true
}

vnode.componentInstance에 값이 있는지 확인한 후에는 다음 두 가지 작업을 수행합니다:

// src/core/vdom/patch.js
function initComponent (vnode, insertedVnodeQueue) {
 if (isDef(vnode.data.pendingInsert)) {
 insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
 vnode.data.pendingInsert = null
 }
 vnode.elm = vnode.componentInstance.$el
 if (isPatchable(vnode)) {
 invokeCreateHooks(vnode, insertedVnodeQueue)
 setScope(vnode)
 } else {
 // empty component root.
 // skip all element-related modules except for ref (#3455)
 registerRef(vnode)
 // make sure to invoke the insert hook
 insertedVnodeQueue.push(vnode)
 }
 }

2. 삽입 메서드를 호출하여 vnode.elm에 저장된 실제 dom 요소를 부모 요소에 삽입합니다.

function insert (parent, elm, ref) {
 if (isDef(parent)) {
 if (isDef(ref)) {
 if (nodeOps.parentNode(ref) === parent) {
 nodeOps.insertBefore(parent, elm, ref)
 }
 } else {
 nodeOps.appendChild(parent, elm)
 }
 }
}

지금까지 초기화 렌더링은 AC 컴포넌트를 정상적으로 렌더링하는 동안 캐싱하고 반복 렌더링에 사용될 때까지 기다립니다.

반복 렌더링(컴퓨팅)

AC 컴포넌트가 다시 활성화되면 컴포넌트의 캐시가 다시 활성화됩니다.

다시 한 번 패치 프로세스를 거치는데, 여기서 patchVnode 메서드가 실행되어 이전 및 새 vnode 노드와 그 자식 노드를 비교하여 논리적 업데이트를 수행합니다.

// src/core/vdom/patch.js
function patchVnode (
 oldVnode,
 vnode,
 insertedVnodeQueue,
 ownerArray,
 index,
 removeOnly
 ) {
 if (oldVnode === vnode) {
 return
 }
 if (isDef(vnode.elm) && isDef(ownerArray)) {
 // clone reused vnode
 vnode = ownerArray[index] = cloneVNode(vnode)
 }
 const elm = vnode.elm = oldVnode.elm
 if (isTrue(oldVnode.isAsyncPlaceholder)) {
 if (isDef(vnode.asyncFactory.resolved)) {
 hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
 } else {
 vnode.isAsyncPlaceholder = true
 }
 return
 }
 // 정적 트리의 재사용된 요소
 // 이 작업은 가상 노드가 복제된 경우에만 수행되며, 새 노드가 복제되지 않으면 핫 리로드 API에 의해 렌더링 기능이 재설정되어 적절한 재렌더링을 수행해야 한다는 의미입니다.
 if (isTrue(vnode.isStatic) &&
 isTrue(oldVnode.isStatic) &&
 vnode.key === oldVnode.key &&
 (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
 ) {
 vnode.componentInstance = oldVnode.componentInstance
 return
 }
 let i
 const data = vnode.data
 if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
 i(oldVnode, vnode)
 }
 const oldCh = oldVnode.children
 const ch = vnode.children
 if (isDef(data) && isPatchable(vnode)) {
 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
 if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
 }
 if (isUndef(vnode.text)) {
 if (isDef(oldCh) && isDef(ch)) {
 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
 } else if (isDef(ch)) {
 if (process.env.NODE_ENV !== 'production') {
 checkDuplicateKeys(ch)
 }
 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
 } else if (isDef(oldCh)) {
 removeVnodes(oldCh, 0, oldCh.length - 1)
 } else if (isDef(oldVnode.text)) {
 nodeOps.setTextContent(elm, '')
 }
 } else if (oldVnode.text !== vnode.text) {
 nodeOps.setTextContent(elm, vnode.text)
 }
 if (isDef(data)) {
 if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
 }
}
// src/core/vdom/create-compoent.js
const componentVNodeHooks = {
 // ...
 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
 /**
 * 새로운 VNode의 컴포넌트 구성 항목
 */
 const options = vnode.componentOptions
 /**
 * 이전 VNode 컴포넌트의 예
 */
 const child = vnode.componentInstance = oldVnode.componentInstance
 /**
 * VNode의 프로퍼티로 자식의 다양한 프로퍼티 업데이트하기
 */
 updateChildComponent(
 child,
 options.propsData, // updated props
 options.listeners, // updated listeners
 vnode, // new parent vnode
 options.children // new children
 )
 }
}

updateChildComponent 메서드는 인스턴스 프로퍼티와 메서드를 업데이트합니다.

// src/core/instance/lifecycle.js
export function updateChildComponent (
 vm: Component,
 propsData: ?Object,
 listeners: ?Object,
 parentVnode: MountedComponentVNode,
 renderChildren: ?Array<VNode>
) {
 if (process.env.NODE_ENV !== 'production') {
 isUpdatingChildComponent = true
 }
 // 컴포넌트에 슬롯 자식이 있는지 확인하려면 컴포넌트의$options._renderChildren 이 작업을 수행합니다.
 
 // 동적 범위 슬롯을 확인합니다. 템플릿에서 컴파일된 정적 범위 슬롯에는 "$stable” 
 const newScopedSlots = parentVnode.data.scopedSlots
 const oldScopedSlots = vm.$scopedSlots
 const hasDynamicScopedSlot = !!(
 (newScopedSlots && !newScopedSlots.$stable) ||
 (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
 (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) ||
 (!newScopedSlots && vm.$scopedSlots.$key)
 )
 // 부모의 하위 정적 슬롯은 부모 업데이트 중에 변경되었을 수 있습니다. 동적 범위의 슬롯도 변경되었을 수 있습니다. 이 경우 업데이트를 강제로 적용해야 정확성을 보장할 수 있습니다.
 const needsForceUpdate = !!(
 // 새로운 정적 슬롯이 존재합니다.
 renderChildren ||
 // 이전 정적 슬롯의 존재
 vm.$options._renderChildren ||
 hasDynamicScopedSlot
 )
 vm.$options._parentVnode = parentVnode
 vm.$vnode = parentVnode // update vm's placeholder node without re-render
 if (vm._vnode) {
 // 자식 노드 트리의 부모 재설정하기
 vm._vnode.parent = parentVnode
 }
 vm.$options._renderChildren = renderChildren
 // 이 함수는 렌더링 중에$attrs $listeners또한 컴포넌트가 사용할 준비가 되었을 때 사용할 수 있도록 활성화 및 비활성화된 두 개의 후크 함수를 추가했습니다.
 vm.$attrs = parentVnode.data.attrs || emptyObject
 vm.$listeners = listeners || emptyObject
 // 소품 업데이트
 if (propsData && vm.$options.props) {
 toggleObserving(false)
 const props = vm._props
 const propKeys = vm.$options._propKeys || []
 for (let i = 0; i < propKeys.length; i++) {
 const key = propKeys[i]
 const propOptions: any = vm.$options.props // wtf flow?
 props[key] = validateProp(key, propOptions, propsData, vm)
 }
 toggleObserving(true)
 // keep a copy of raw propsData
 vm.$options.propsData = propsData
 }
 // 리스너 업데이트
 listeners = listeners || emptyObject
 const oldListeners = vm.$options._parentListeners
 vm.$options._parentListeners = listeners
 updateComponentListeners(vm, listeners, oldListeners)
 // 슬롯 속성 업데이트+ 강제 업데이트
 if (needsForceUpdate) {
 vm.$slots = resolveSlots(renderChildren, parentVnode.context)
 vm.$forceUpdate()
 }
 if (process.env.NODE_ENV !== 'production') {
 isUpdatingChildComponent = false
 }
}

또한 컴포넌트가 슬롯을 감싸고 있기 때문에 updateChildComponent 메서드가 실행되는 동안 needForceUpdate가 true가 되면 인스턴스의 슬롯 속성을 업데이트한 다음 vm.$forceUpdate 메서드가 실행됩니다.

if (needsForceUpdate) {
 vm.$slots = resolveSlots(renderChildren, parentVnode.context)
 vm.$forceUpdate()
}
Vue.prototype.$forceUpdate = function () {
 const vm: Component = this
 if (vm._watcher) {
 vm._watcher.update()
 }
 }

vm.$forceUpdate 메서드는 구성 요소의 렌더 메서드를 다시 실행하는 vm._watcher.update 메서드를 실행합니다. 현재 활성화된 AC 컴포넌트는 초기화 단계에서 캐시되었으므로 렌더 메서드가 실행되는 동안 캐시된 componentInstance가 vnode.componentInstance에 복사되고 키 목록이 업데이트되어 최종적으로 노드에 반환됩니다.

if (cache[key]) {
 vnode.componentInstance = cache[key].componentInstance
 // make current key freshest
 remove(keys, key)
 keys.push(key)
 } else {
 // delay setting the cache until update
 this.vnodeToCache = vnode
 this.keyToCache = key
 }

렌더 메서드는 실행을 완료하고 노드의 패치 마운트 단계로 진행합니다. 여기서 createComponent 메서드가 실행되고 그 다음에는 init 메서드가 실행됩니다. 이 시점에서 다양한 조건이 충족되어 프리패치 메서드가 실행되고, 이 메서드는 업데이트ChildComponent 메서드를 호출하여 인스턴스의 프로퍼티와 메서드를 업데이트합니다.

이 init 메서드의 실행은 렌더링을 초기화할 때처럼 컴포넌트를 인스턴스화하지 않고 $mount 메서드를 실행하므로 생성되고 마운트된 후크 함수를 실행하지 않습니다.

// src/core/vdom/create-compoent.js
const componentVNodeHooks = {
 init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
 if (
 vnode.componentInstance &&
 !vnode.componentInstance._isDestroyed &&
 vnode.data.keepAlive
 ) {
 // kept-alive components, treat as a patch
 const mountedNode: any = vnode // work around flow
 componentVNodeHooks.prepatch(mountedNode, mountedNode)
 } else {
 const child = vnode.componentInstance = createComponentInstanceForVnode(
 vnode,
 activeInstance
 )
 child.$mount(hydrating ? vnode.elm : undefined, hydrating)
 }
 },
 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
 /**
 * 새로운 VNode의 컴포넌트 구성 항목
 */
 const options = vnode.componentOptions
 /**
 * 이전 VNode 컴포넌트의 예
 */
 const child = vnode.componentInstance = oldVnode.componentInstance
 /**
 * VNode의 프로퍼티로 자식의 다양한 프로퍼티 업데이트하기
 */
 updateChildComponent(
 child,
 options.propsData, // updated props
 options.listeners, // updated listeners
 vnode, // new parent vnode
 options.children // new children
 )
 },
 // ...
}

init 메서드의 실행이 완료되면 createComponent 메서드로 돌아가 후속 코드 실행을 진행하며, 이 시점에서 isReactivated가 true가 됩니다.

if (isDef(vnode.componentInstance)) {
 initComponent(vnode, insertedVnodeQueue)
 insert(parentElm, vnode.elm, refElm)
 if (isTrue(isReactivated)) {
 reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
 }
 return true
}

createComponent를 반환하면 실제로 세 가지 작업이 수행됩니다:

  1. vnode.elm에 값을 다시 할당합니다.
  2. 부모 요소에 vnode.elm을 삽입합니다.
  3. 캐시된 노드와 관련된 애니메이션을 수행합니다.

마지막으로 패치로 돌아가서 removeVnodes 메서드를 실행하여 이전 노드를 제거합니다. 이것으로 캐시 렌더링 프로세스가 끝났습니다.

라이프사이클 함수

이 컴포넌트는 캐시된 컴포넌트에 활성화 및 비활성화의 두 가지 수명 주기 함수를 추가합니다.

activated

패치 메서드에서는 invokeInsertHook 메서드가 실행됩니다.

// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
 // 삽입 후크의 실행을 지연시키고 요소가 삽입될 때까지 기다렸다가 삽입 후크 함수를 루트 노드에 삽입합니다.
 if (isTrue(initial) && isDef(vnode.parent)) {
 vnode.parent.data.pendingInsert = queue
 } else {
 for (let i = 0; i < queue.length; ++i) {
 queue[i].data.hook.insert(queue[i])
 }
 }
}

삽입 메서드는 invokeInsertHook 메서드에서 호출됩니다.

// src/core/vdom/create-component.js
insert (vnode: MountedComponentVNode) {
 const { context, componentInstance } = vnode
 if (!componentInstance._isMounted) {
 componentInstance._isMounted = true
 callHook(componentInstance, 'mounted')
 }
 if (vnode.data.keepAlive) {
 if (context._isMounted) {
 // 업데이트 중에 활성 상태를 유지하는 컴포넌트의 하위 컴포넌트가 변경될 수 있습니다.,
 // 따라서 여기서 트리를 직접 탐색하면 잘못된 하위 컴포넌트에서 활성화된 후크를 호출할 수 있습니다.
 // 대신 전체 패치 프로세스가 끝날 때 처리될 대기열로 푸시됩니다.
 queueActivatedComponent(componentInstance)
 } else {
 activateChildComponent(componentInstance, true /* direct */)
 }
 }
}

삽입 메서드에서는 노드가 렌더링되었는지 여부를 판단하여 렌더링된 경우 queueActivatedComponent를 실행하고, 렌더링되지 않은 경우 activateChildComponent 메서드를 실행합니다.

queueActivatedComponent 메서드에서 현재 인스턴스에 대한 후크 함수는 캐시만 되고 실행을 위해 실행 스택에 즉시 추가되지 않습니다.

// src/core/oberser/scheduler.js
export function queueActivatedComponent (vm: Component) {
 // setting _inactive to false here so that a render function can
 // rely on checking whether it's in an inactive tree (e.g. router-view)
 vm._inactive = false
 activatedChildren.push(vm)
}
// src/core/oberser/scheduler.js
function flushSchedulerQueue () {
 // ...
 // keep copies of post queues before resetting state
 const activatedQueue = activatedChildren.slice()
 const updatedQueue = queue.slice()
 resetSchedulerState()
 // call component updated and activated hooks
 callActivatedHooks(activatedQueue)
 callUpdatedHooks(updatedQueue)
 // ...
}
// src/core/oberser/scheduler.js
function callActivatedHooks (queue) {
 for (let i = 0; i < queue.length; i++) {
 queue[i]._inactive = true
 activateChildComponent(queue[i], true /* true */)
 }
}
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
 if (direct) {
 vm._directInactive = false
 if (isInInactiveTree(vm)) {
 return
 }
 } else if (vm._directInactive) {
 return
 }
 if (vm._inactive || vm._inactive === null) {
 vm._inactive = false
 for (let i = 0; i < vm.$children.length; i++) {
 activateChildComponent(vm.$children[i])
 }
 callHook(vm, 'activated')
 }
}
deactivated
// src/core/vdom/patch.js
function removeVnodes (vnodes, startIdx, endIdx) {
 for (; startIdx <= endIdx; ++startIdx) {
 const ch = vnodes[startIdx]
 if (isDef(ch)) {
 if (isDef(ch.tag)) {
 removeAndInvokeRemoveHook(ch)
 invokeDestroyHook(ch)
 } else { // Text node
 removeNode(ch.elm)
 }
 }
 }
}
// src/core/vdom/patch.js
function invokeDestroyHook (vnode) {
 let i, j
 const data = vnode.data
 if (isDef(data)) {
 if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
 for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
 }
 if (isDef(i = vnode.children)) {
 for (j = 0; j < vnode.children.length; ++j) {
 invokeDestroyHook(vnode.children[j])
 }
 }
 }
// src/core/vdom/create-component.js
destroy (vnode: MountedComponentVNode) {
 const { componentInstance } = vnode
 if (!componentInstance._isDestroyed) {
 if (!vnode.data.keepAlive) {
 componentInstance.$destroy()
 } else {
 deactivateChildComponent(componentInstance, true /* direct */)
 }
 }
}

요약

컴포넌트는 추상 컴포넌트이며 인터페이스에 렌더링되지 않습니다. 렌더링된 노드를 초기화할 때 LRU 전략을 사용하여 컴포넌트의 vnode를 캐시하고 노드가 다시 렌더링되면 캐시에서 노드를 읽어 인터페이스에 렌더링하여 상태 캐싱을 달성하고 동시에 노드 사용 시 컴포넌트가 사용할 수 있는 활성화 및 비활성화의 두 가지 후크 함수를 추가했습니다.

Read next

4차원의 ChatGPT 기술 원리, 신비한 기술 블랙박스 첫 번째 호 공개

기사 카탈로그\n보물 발견하기\n미리 쓰기\n3. 큐 러닝과 빅 모델 기능의 등장\n글쓰기 끝\n\n보물 발견하기\n얼마 전 이해하기 쉽고 재미있는 거대한 인공지능 학습 웹사이트를 발견해서 여러분과 공유하지 않을 수 없었습니다. 트레저 엔트리입니다.\n앞에 쓰기\n\n채팅

Oct 15, 2025 · 7 min read