Vue.js instance lifecycle은 어떻게 초기화 될까?

👊0👊·2020년 1월 8일
1

vue.js 공부 정리

목록 보기
1/1

vue.js 라이프사이클을 공부하다. 어떻게 작동되는지 궁금해서 살짝 코드 좀 뒤져봤다.

Vue.js 라이프사이클

vue 인스턴스 라이크 사이클은 new Vue() 로 인스턴스를 생성 했을 생성부터 소멸까지 거치는 내부 과정을 말합니다.

lifecycle.png

Instance 생성

new Vue()

로 Vue를 생성하면 아래의 코드가 실행된다.

// vue/src/core/instance/index.js
...

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

우리가 {}로 설정한 _init을 처음실행한다.

// vue/src/core/instance/init.js
...
let uid = 0
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++
    
    ...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
  	...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }    

보면 beforeCretea Hook은 lifecylce, event, render가 초기화 된 다음 실행되는 것을 볼 수있다.
그리고 injection, state, proive가 초기화 되고 create훅이 호출된다. 정말 그림과 같다는 것을 볼 수 있다.

initState 함수는 우리가 this로 접근하는 vm의 옵션들(props,methods,computed,watch,data)를 초기화한다.
그렇기 때문에 created Hook에선 옵션들(props,methods,computed,watch,data)에 접근할 수 있게 된다.

// vue/src/core/instance/state.js
    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }

image.png

inject와 provie는 잘 쓰는 옵션은 아니라서 따로 링크를 달아놓는다.
https://kr.vuejs.org/v2/api/index.html

(2) Mounting: 돔(DOM) 삽입 단계

Mounting 단계는 초기 렌더링 직전에 컴포넌트에 직접 접근할 수 있다. 서버렌더링에서는 지원하지 않는다.

초기 랜더링 직전에 돔을 변경하고자 한다면 이 단계를 활용할 수 있다. 그러나 컴포넌트 초기에 세팅되어야할 데이터 페치는 created 단계를 사용하는것이 낫다.

    		callHook(vm, 'created')
    
    		...
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }

웹에선 vm.$mount는 다음과 같이 작동된다.

beforeMounted Hook

    // vue/src/platforms/web/runtime/index.js 
    // public mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }

beforeMount훅 전에는 템플릿이 있는지 없는지 확인한 후 템플릿을 렌더링 한 상태이므로, 가상 DOM이 생성되어 있으나 실제 DOM에 부착되지는 않은 상태입니다.

    // vue/src/core/instance/lifecycle.js
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode // 가상 Dom을 render에 등록함
        ...
      }
      callHook(vm, 'beforeMount')

그 다음 update를 미리 Watcher를 만들어 미리 감시한다.

      // vue/src/core/instance/lifecycle.js
    	let updateComponent
    	...// updateComponent 초기화됨 
    	// mount전에 미리 updateComponet를 감시하고 있는 것을 알 수 있다.
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false
    

mounted Hook

그 다음 가상DOM이 null이면 mount를 하고 mount hook을 호출한다.
즉 $el을 사용하여 DOM에 접근 할 수 있다.
하지만, mounted 훅이 호출되었다고 모든 컴포넌트가 마운트 되었다고 보장할 수는 없다.

만약 전체가 렌더링 된 후 작업을 시작하려면 $nextTick을 사용해야 한다.

    // vue/src/core/instance/lifecycle.js  
    	// manually mounted instance, call mounted on self
      // mounted is called for render-created child components in its inserted hook
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }

(3) Updating

컴포넌트에서 사용되는 속성들이 변경되는 등의.. 컴포넌트가 재 랜더링 되면 실행되는 라이프 사이클 훅입니다. 컴포넌트가 재 렌더링 될 때, 변경 된 값으로 어떠한 작업을 해야 할 때 유용하게 사용 되는 훅입니다. 서버 사이드 렌더링은 지원하지 않습니다.

beforeUpdate

DOM이 재 렌더링 되기 직전에 호출되는 라이프 사이클 훅입니다. 업데이트 된 값들을 가지고 있는 상태이기 때문에, 업데이트 된 값으로 다른 값들을 업데이트 할 수 있습니다. 이 훅에서 값이 변경되더라도 다시 beforeUpdate 훅이 호출 되지 않기 때문에, 무한 루프에 빠질 걱정은 하지 않으셔도 됩니다.

위에서 beforeMount와 mounted 훅 사이에 updateComponet가 초기화 된것을 보았다.

    updateComponent = () => {
    		vm._update(vm._render(), hydrating)
    }

vm._update는 lifecyleMinxin에 정의되어 있다.

    export function lifecycleMixin (Vue: Class<Component>) {
      Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        const prevEl = vm.$el
        const prevVnode = vm._vnode
        const restoreActiveInstance = setActiveInstance(vm)
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        restoreActiveInstance()
        // update __vue__ reference
        if (prevEl) {
          prevEl.__vue__ = null
        }
        if (vm.$el) {
          vm.$el.__vue__ = vm
        }
        // if parent is an HOC, update its $el as well
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }
    
      Vue.prototype.$forceUpdate = function () {
        const vm: Component = this
        if (vm._watcher) {
          vm._watcher.update()
        }
      }
    
     ...
    }

beforeUpdate

이 훅은 컴포넌트의 데이터가 변하여 업데이트 사이클이 시작될때 실행된다. 정확히는 돔이 재 렌더링되고 패치되기 직전에 실행된다. 재 렌더링 전의 새 상태의 데이터를 얻을 수 있고 더 많은 변경이 가능하다. 이 변경으로 이한 재 렌더링은 트리거되지 않는다.

updated

이 훅은 컴포넌트의 데이터가 변하여 재 렌더링이 일어나 후에 실행된다. 돔이 업데이트 완료된 상태이므로 돔 종속적인 연산을 할 수 있다. 그러나 여기서 상태를 변경하면 무한루프에 빠질 수 있다. 모든 자식 컴포넌트의 재 렌더링 상태를 보장하지는 않는다.

    new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false
    
      // manually mounted instance, call mounted on self
      // mounted is called for render-created child components in its inserted hook
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm

(4) Destroying

    Vue.prototype.$destroy = function () {
        const vm: Component = this
        if (vm._isBeingDestroyed) {
          return
        }
        callHook(vm, 'beforeDestroy')
        vm._isBeingDestroyed = true
        // remove self from parent
        const parent = vm.$parent
        if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
          remove(parent.$children, vm)
        }
        // teardown watchers
        if (vm._watcher) {
          vm._watcher.teardown()
        }
        let i = vm._watchers.length
        while (i--) {
          vm._watchers[i].teardown()
        }
        // remove reference from data ob
        // frozen object may not have observer.
        if (vm._data.__ob__) {
          vm._data.__ob__.vmCount--
        }
        // call the last hook...
        vm._isDestroyed = true
        // invoke destroy hooks on current rendered tree
        vm.__patch__(vm._vnode, null)
        // fire destroyed hook
        callHook(vm, 'destroyed')
        // turn off all instance listeners.
        vm.$off()
        // remove __vue__ reference
        if (vm.$el) {
          vm.$el.__vue__ = null
        }
        // release circular reference (#6759)
        if (vm.$vnode) {
          vm.$vnode.parent = null
        }
      }

후기

좀 더 코드를 정리해서 쓰고 싶은데, 너무 작성할께 많아서 귀찮아졌다. 그냥 여기까지 이해하고 써야겠다.

참고

profile
ㅎㅎ

0개의 댓글