結合源碼聊一聊Vue的生命週期

生命週期的是咱們在開發中不可迴避的話題。瞭解生命週期也可讓咱們知道什麼階段能夠作什麼,以及更好的解決項目中遇到的問題。
本文包含我的理解內容,但願你們批判性閱讀,若有問題歡迎交流~


文章說明

  1. 每個 · 後跟的生命週期鉤子能夠點擊進入Vue源碼的調用函數或代碼行。
  2. 上述生命週期鉤子後的加粗字體是Vue文檔對鉤子函數的簡要解釋。
  3. 文中引入的Vue源碼均進行了不一樣程度的簡化,僅供參考,詳細代碼能夠經過第一條說明位置點擊查看。

beforeCreate & created


Vue.prototype._init = function (options?: Object) {
  // ...
  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')
  // ...
}複製代碼

上面的代碼是Vue實例化時調用的方法,從代碼中咱們能夠看到,Vue的實例化階段執行了 beforeCreate 和 created 兩個鉤子函數,下面分別來講。

beforeCreate

  • beforeCreate官方的解釋是,在實例(Vue)初始化以後,數據觀測(data observer)和 event/watcher 事件配置以前被調用
從上面的代碼裏能夠看到,在調用 beforeCreate 以前,調用了三個函數,分別是初始化生命週期、事件和render。須要注意的是,此處的 initEvents(點擊查看源碼) 初始化的並非自定義的事件,而是Vue一些原生事件和方法。
因此此時定義在 data 中的屬性、methods中的方法等等都還不能訪問。

created

  • created:官方解釋是在實例建立完成後被當即調用。在這一步,實例已完成如下的配置:數據觀測 (data observer),property 和方法的運算,watch/event 事件回調。然而,掛載階段還沒開始,$el property 目前尚不可用。
在 beforeCreate以後,created以前,執行了 initInjections(vm)、initState(vm)、initProvide(vm) 三個方法
可是此處留一個疑問,我也沒有搞明白,爲何是先初始化 inject,後初始化 provide?歡迎大佬們指導(抱拳.jpg)

從上面調用的方法能夠看到, 在 created 階段,咱們已經能夠訪問到自定義的一些 數據、屬性和方法等內容,可是依然沒有DOM。此階段咱們能夠獲取一些頁面初始化時就須要顯示的數據,可是不能操做DOM。

beforeMount & mounted


export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component {
  vm.$el = el
  ......
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // 須要對組件渲染進行性能追蹤時執行邏輯
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  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
}複製代碼

beforeMount

  • beforeMount:在掛載以前被調用:相關的render函數首次被調用(該鉤子在服務端渲染期間不被調用)
在 created 以後,beforeMount 以前,會檢查 el 屬性,el 屬性決定了咱們最後要把DOM掛載到哪兒,若是不存在 el,則檢查是否手動調用了 vm.$mount(el) 。兩個條件知足其一,則進行下一步,不然中止執行。下一步會檢查 template 屬性是否存在,若是不存在則檢查外層是否存在知足 el 傳入選擇器條件的 HTML 元素,兩個條件知足其一,則進入 mount 過程,不然報錯。

知足以上條件後,調用 beforeMount 鉤子

beforeMount 以後,經過 vm._render() 將代碼渲染爲 VNode,而後經過 vm._update() 將 VNode patch 到真實的 DOM。完成後執行 mounted 鉤子。

mounted

  • mounted:實例被掛載之後調用,el 被替換爲 vm.$el
mounted 不會保證全部的子組件都掛載完成。若是但願等到整個視圖都渲染完畢,可使用 $nextTick

mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}
複製代碼
這裏有一點容易懵逼的是,在Vue文檔中寫的是在 beforeMount 以後用新建立的 vm.$el 替換 el,可是上面的源碼中卻看到在 beforeMount 以前執行了 vm.$el = el 。關於這個問題,在測試beforeMount 和 mounted 兩個鉤子中輸出 $el 後,個人我的理解是:
  1. 在 beforeMount 以前對 $el 的賦值只是把經過 el 選擇器拿到的DOM給了 $el,但此時並無咱們寫的其餘頁面內容,因此拿到至關於只是一個空殼。
  2. 在 beforeMount 以後,將代碼渲染爲 VNode,並經過 vm._update() patch 到真實DOM,經過代碼能夠看到,在 vm._update 中是更新過 vm.$el 的,因此此時的 $el 拿到的纔是完成的 DOM 結構。所以文檔說的是在 beforeMount 以後將 el 替換爲新建立的 $el。

因此,這兩個生命週期的執行邏輯能夠總結爲:
  1. vm.$el = el
  2. 執行 beforeMount()
  3. 調用 vm._render() 渲染 VNode
  4. vm._update() 把 VNode patch 到真實的 DOM,並更新 $el
  5. 執行 mounted()

beforeUpdate & updated


// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
/** * Flush both queues and run the watchers. */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  // created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  // user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  // its watchers can be skipped.
  
  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}複製代碼

beforeUpdate

  • beforeUpdate:數據更新時調用,發生在虛擬DOM打補丁以前。這裏適合在更新以前訪問現有的DOM
上面代碼對 vue 實例建立了一個監聽,並將 updateComponent 做爲回調,在實例數據有更新時去更新DOM。

但在此以前,有一個判斷條件,也就是 before 參數中的內容,首先判斷當前 _isMounted 爲 true,也就是保證如今組件已經 mounted,同時 _isDestoryed 爲 false,也就是保證如今組件數據不是由於要銷燬才發生的改變。知足這兩個條件後調用 beforeMount 鉤子。

updated

  • updated:數據更改致使的虛擬DOM從新渲染和打補丁,以後調用該鉤子。此時組件的DOM已經更新,因此能夠執行依賴於DOM的操做
updated 調用在 callUpdatedHooks() 方法中,callUpdatedHooks() 在 flushSchedulerQueue() 中被調用。

flushSchedulerQueue() 主要是對要更新的隊列進行預處理,從上面代碼保留的註釋中咱們能夠看到簡要的處理邏輯。

在預處理完成後,將處理過的隊列做爲參數調用 callUpdatedHooks() ,方法內部對更新隊列進行遍歷,而後對知足條件的隊列調用 updated 鉤子。

beforeDestroy & destroy


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
    }
  }
}
複製代碼


beforeDestroy

  • beforeDestroy:實例銷燬以前調用。在這一步,實例徹底可用
beforeDestroy 在 $destroy 方法最開始調用,此時銷燬尚未開始,因此當前實例徹底可用。

beforeDestroy 以後,將 _isBeingDestroyed 置爲true,同時開始執行一系列的銷燬過程,主要包括:從當前組件的 $parent 中刪除本身、移除watch、調用當前渲染 VNode 的銷燬鉤子。

上述過程執行完之後,調用 destroyed 鉤子。

destoryed

  • destroyed:實例銷燬後調用。該鉤子被調用後,對應的Vue實例全部指令都被解綁,全部的事件監聽器被移除,全部的子實例被銷燬

至此,生命週期介紹完了,經過了解生命週期,咱們能夠簡單的總結出如下幾點:
  1. 在created中能夠訪問到自定義的數據、方法、計算屬性、監聽等內容,可是沒有DOM,此時咱們能夠進行頁面渲染所需數據的獲取工做。
  2. 執行mounted時,DOM已經渲染完成,能夠進行DOM的更新動做。
  3. 在destroyed中能夠進行DOM的銷燬工做。

瞭解了每一個階段能夠作什麼,可以很大程度上減小咱們寫代碼中遇到的問題。如發現文章中的問題,歡迎交流、指出~


參考內容:javascript

  1. 《Vue.js 技術揭祕》
  2. 《Vue官方文檔》

感謝大佬們文章的幫助~html

相關文章
相關標籤/搜索