vue.js 라이프사이클을 공부하다. 어떻게 작동되는지 궁금해서 살짝 코드 좀 뒤져봤다.
vue 인스턴스 라이크 사이클은 new Vue() 로 인스턴스를 생성 했을 생성부터 소멸까지 거치는 내부 과정을 말합니다.
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)
}
}
inject와 provie는 잘 쓰는 옵션은 아니라서 따로 링크를 달아놓는다.
https://kr.vuejs.org/v2/api/index.html
Mounting 단계는 초기 렌더링 직전에 컴포넌트에 직접 접근할 수 있다. 서버렌더링에서는 지원하지 않는다.
초기 랜더링 직전에 돔을 변경하고자 한다면 이 단계를 활용할 수 있다. 그러나 컴포넌트 초기에 세팅되어야할 데이터 페치는 created 단계를 사용하는것이 낫다.
callHook(vm, 'created')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
웹에선 vm.$mount는 다음과 같이 작동된다.
// 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
그 다음 가상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
}
컴포넌트에서 사용되는 속성들이 변경되는 등의.. 컴포넌트가 재 랜더링 되면 실행되는 라이프 사이클 훅입니다. 컴포넌트가 재 렌더링 될 때, 변경 된 값으로 어떠한 작업을 해야 할 때 유용하게 사용 되는 훅입니다. 서버 사이드 렌더링은 지원하지 않습니다.
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()
}
}
...
}
이 훅은 컴포넌트의 데이터가 변하여 업데이트 사이클이 시작될때 실행된다. 정확히는 돔이 재 렌더링되고 패치되기 직전에 실행된다. 재 렌더링 전의 새 상태의 데이터를 얻을 수 있고 더 많은 변경이 가능하다. 이 변경으로 이한 재 렌더링은 트리거되지 않는다.
이 훅은 컴포넌트의 데이터가 변하여 재 렌더링이 일어나 후에 실행된다. 돔이 업데이트 완료된 상태이므로 돔 종속적인 연산을 할 수 있다. 그러나 여기서 상태를 변경하면 무한루프에 빠질 수 있다. 모든 자식 컴포넌트의 재 렌더링 상태를 보장하지는 않는다.
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
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
}
}
좀 더 코드를 정리해서 쓰고 싶은데, 너무 작성할께 많아서 귀찮아졌다. 그냥 여기까지 이해하고 써야겠다.