從源碼解讀Vue生命週期,讓面試官對你另眼相看

觀感度:🌟🌟🌟🌟🌟javascript

口味:蜜桃烏龍前端

烹飪時間:30minvue




在咱們的實際項目中,與Vue的生命週期打交道能夠說是屢見不鮮。掌握Vue的生命週期對開發者來講是特別重要的。那麼若是可以從源碼角度理解Vue的生命週期,對咱們的開發和成長會有進一步的提高。

本文從基礎知識開始講起,分爲基礎知識和源碼解讀兩部分,對基礎知識已經掌握的開發者可自行跳躍。java

基礎知識

Vue的生命週期

大天然有春夏秋冬,人有生老病死,優秀的Vue固然也存在本身的生命週期。node

對於Vue來講它的生命週期就是Vue實例從建立到銷燬的過程git

生命週期函數

在生命週期的過程當中運行着一些叫作生命週期的函數,給予了開發者在不一樣的生命週期階段添加業務代碼的能力。github

在網上的一些文章中有的也叫它們生命週期鉤子,那鉤子又是什麼呢?ajax

鉤子函數

其實和回調是一個概念,當系統執行到某處時,檢查是否有hook(鉤子),有的話就會執行回調。服務器

此hook非彼hook。app

通俗的說,hook就是在程序運行中,在某個特定的位置,框架的開發者設計好了一個鉤子來告訴咱們當前程序已經運行到特定的位置了,會觸發一個回調函數,並提供給咱們,讓咱們能夠在生命週期的特定階段進行相關業務代碼的編寫。

我在官方提供的圖片上添加了相關注釋,但願可以讓你們看的更明白一些,以下圖。

雖然添加了不少註釋,看不懂不要慌,咱們來逐一進行講解。

總的來講,Vue的生命週期能夠分爲如下八個階段:

beforeCreate 實例建立前

created 實例建立完成

beforeMount 掛載前

mounted 掛載完成

beforeUpdate 更新前

updated 更新完成

beforeDestory 銷燬前

destoryed 銷燬完成

1.beforeCreate

這個鉤子是new Vue()以後觸發的第一個鉤子,在當前階段中data、methods、computed以及watch上的數據和方法均不能被訪問。

2.created

這個鉤子在實例建立完成後發生,當前階段已經完成了數據觀測,也就是可使用數據,更改數據,在這裏更改數據不會觸發updated函數。能夠作一些初始數據的獲取,注意請求數據不易過多,會形成白屏時間過長。在當前階段沒法與Dom進行交互,若是你非要想,能夠經過vm.$nextTick來訪問Dom。

3.beforeMounted

這個鉤子發生在掛載以前,在這以前template模板已導入渲染函數編譯。而當前階段虛擬Dom已經建立完成,即將開始渲染。在此時也能夠對數據進行更改,不會觸發updated。

4.mounted

這個鉤子在掛載完成後發生,在當前階段,真實的Dom掛載完畢,數據完成雙向綁定,能夠訪問到Dom節點,使用$ref屬性對Dom進行操做。也能夠向後臺發送請求,拿到返回數據。

5.beforeUpdate

這個鉤子發生在更新以前,也就是響應式數據發生更新,虛擬dom從新渲染以前被觸發,你能夠在當前階段進行更改數據,不會形成重渲染。

6.updated

這個鉤子發生在更新完成以後,當前階段組件Dom已完成更新。要注意的是避免在此期間更改數據,由於這可能會致使無限循環的更新。

7.beforeDestroy

這個鉤子發生在實例銷燬以前,在當前階段實例徹底能夠被使用,咱們能夠在這時進行善後收尾工做,好比清除計時器。

8.destroyed

這個鉤子發生在實例銷燬以後,這個時候只剩下了dom空殼。組件已被拆解,數據綁定被卸除,監聽被移出,子實例也通通被銷燬。

注意點

在使用生命週期時有幾點注意事項須要咱們牢記。

1.第一點就是上文曾提到的created階段的ajax請求與mounted請求的區別:前者頁面視圖未出現,若是請求信息過多,頁面會長時間處於白屏狀態。

2.除了beforeCreate和created鉤子以外,其餘鉤子均在服務器端渲染期間不被調用。

3.上文曾提到過,在updated的時候千萬不要去修改data裏面賦值的數據,不然會致使死循環。

4.Vue的全部生命週期函數都是自動綁定到this的上下文上。因此,你這裏使用箭頭函數的話,就會出現this指向的父級做用域,就會報錯。緣由下面源碼部分會講解。

源碼解讀

由於Vue的源碼部分包含不少內容,本文只選取生命週期相關的關鍵性代碼進行解析。同時也強烈推薦你們學習Vue源碼的其餘內容,由於這個框架真的很優秀,附上連接[Vue.js技術揭祕](ustbhuangyi.github.io/vue-analysi…)。

咱們先來從源碼中來解答上文注意點的第四個問題(如下全部代碼都有刪減,用...代替刪減部分)。

// src/core/instance/lifecycle.js
// callhook 函數的功能就是在當前vue組件實例中,調用某個生命週期鉤子註冊的全部回調函數。
// vm:Vue實例
// hook:生命週期名字
export function callHook (vm: Component, hook: string) {
  pushTarget()
  const handlers = vm.$options[hook] 
  // 初始化合並 options 的過程 、,將各個生命週期函數合併到 options 裏
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

// src/core/util/error.js
export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
複製代碼

咱們從上面的代碼中能夠看到callHook中調用了invokeWithErrorHandling方法,在invokeWithErrorHandling方法中,使用了apply和call改變了this指向,而在箭頭函數中this指向是沒法改變的,因此咱們在編寫生命週期函數的時候不能使用箭頭函數。關於this指向問題請移步個人另外一篇文章[治療this「皮」的詳細藥方](juejin.im/post/5d15b7…)。

解答完上面遺留的問題後,咱們再來逐一講解各個生命週期。

1.beforeCreate和created

// src/core/instance/init
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    // 合併選項部分已省略
    
    initLifecycle(vm)  
    // 主要就是給vm對象添加了 $parent、$root、$children 屬性,以及一些其它的生命週期相關的標識
    initEvents(vm) // 初始化事件相關的屬性
    initRender(vm)  // vm 添加了一些虛擬 dom、slot 等相關的屬性和方法
    callHook(vm, 'beforeCreate')  // 調用 beforeCreate 鉤子
    //下面 initInjections(vm) 和 initProvide(vm) 兩個配套使用,用於將父組件 _provided 中定義的值,經過 inject 注入到子組件,且這些屬性不會被觀察
    initInjections(vm) 
    initState(vm)   // props、methods、data、watch、computed等數據初始化
    initProvide(vm) 
    callHook(vm, 'created')  // 調用 created 鉤子
  }
}

// src/core/instance/state
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)
  }
}
複製代碼

咱們能夠看到beforeCreate鉤子調用是在initState以前的,而從上面的第二段代碼咱們能夠看出initState的做用是對props、methods、data、computed、watch等屬性作初始化處理。

經過閱讀源碼,咱們更加清楚的明白了在beforeCreate鉤子的時候咱們沒有對props、methods、data、computed、watch上的數據的訪問權限。在created中才能夠。

2.beforeMount和mounted

// mountComponent 核心就是先實例化一個渲染Watcher
// 在它的回調函數中會調用 updateComponent 方法
// 兩個核心方法 vm._render(生成虛擬Dom) 和 vm._update(映射到真實Dom)
// src/core/instance/lifecycle
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    ...
  }
  callHook(vm, 'beforeMount')  // 調用 beforeMount 鉤子

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    // 將虛擬 Dom 映射到真實 Dom 的函數。
    // vm._update 以前會先調用 vm._render() 函數渲染 VNode
      ...
      const vnode = vm._render()
      ...
      vm._update(vnode, hydrating)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判斷是否 mouted 完成 而且沒有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')  //調用 mounted 鉤子
  }
  return vm
}
複製代碼

經過上面的代碼,咱們能夠看出在執行vm._render()函數渲染VNode以前,執行了 beforeMount鉤子函數,在執行完 vm._update()把VNode patch到真實Dom後,執行 mouted鉤子。也就明白了爲何直到mounted階段才名正言順的拿到了Dom。

3.beforeUpdate和updated

// src/core/instance/lifecycle
 new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判斷是否 mouted 完成 而且沒有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')  // 調用 beforeUpdate 鉤子
      }
    }
  }, true /* isRenderWatcher */)
 
 // src/core/observer/scheduler 
 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) {
       // 只有知足當前 watcher 爲 vm._watcher(也就是當前的渲染watcher)
       // 以及組件已經 mounted 而且沒有被 destroyed 纔會執行 updated 鉤子函數。
       callHook(vm, 'updated')  // 調用 updated 鉤子
       }
     }
   }
複製代碼

第一段代碼就是在beforeMount和mounted鉤子中間出現的,那麼watcher中究竟作了些什麼呢?第二段代碼的callUpdatedHooks函數中何時才能夠知足條件並執行updated呢?咱們來接着往下看。

// src/instance/observer/watcher.js
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    // 在它的構造函數裏會判斷 isRenderWatcher,
    // 接着把當前 watcher 的實例賦值給 vm._watcher
    isRenderWatcher?: boolean
  ) {
    // 還把當前 wathcer 實例 push 到 vm._watchers 中,
    // vm._watcher 是專門用來監聽 vm 上數據變化而後從新渲染的,
    // 因此它是一個渲染相關的 watcher,所以在 callUpdatedHooks 函數中,
    // 只有 vm._watcher 的回調執行完畢後,纔會執行 updated 鉤子函數
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    ...
}
複製代碼

看到這裏咱們明白了Vue是經過watcher來監聽實例上的數據變化,進而控制渲染流程。

4.beforeDestroy和destroyed

// src/core/instance/lifecycle.js
  // 在 $destroy 的執行過程當中,它會執行 vm.__patch__(vm._vnode, null)
  // 觸發它子組件的銷燬鉤子函數,這樣一層層的遞歸調用,
  // 因此 destroy 鉤子函數執行順序是先子後父,和 mounted 過程同樣。
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')  // 調用 beforeDestroy 鉤子
    vm._isBeingDestroyed = true
    // 一些銷燬工做
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 拆卸 watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    ...
    vm._isDestroyed = true
    // 調用當前 rendered tree 上的 destroy 鉤子
    // 發現子組件,會先去銷燬子組件
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')  // 調用 destroyed 鉤子
    // 關閉全部實例偵聽器。
    vm.$off()
    // 刪除 __vue__ 引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 釋放循環引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
複製代碼

經過上面的代碼,咱們瞭解了組件銷燬階段的拆卸過程,其中會執行一個__patch__函數,講解起來篇幅較多,想要深刻了解該部分的同窗能夠自行閱讀源碼解讀處給你們的連接。

除了這八種鉤子外,咱們在官網也能夠查閱到另外幾種不經常使用的鉤子,這裏列舉出來。

幾種不經常使用的鉤子

activated

keep-alive 組件激活時調用,該鉤子在服務器端渲染期間不被調用。

deactivated

keep-alive 組件停用時調用,該鉤子在服務器端渲染期間不被調用。

errorCaptured

當捕獲一個來自子孫組件的錯誤時被調用。此鉤子會收到三個參數:錯誤對象、發生錯誤的組件實例以及一個包含錯誤來源信息的字符串。此鉤子能夠返回 false 以阻止該錯誤繼續向上傳播

你能夠在此鉤子中修改組件的狀態。所以在模板或渲染函數中設置其它內容的短路條件很是重要,它能夠防止當一個錯誤被捕獲時該組件進入一個無限的渲染循環。

交流

歡迎來個人我的公衆號交流,優質原創文章將同步推送。後臺回覆福利,便可領取福利,你懂得~

你的前端食堂,記得按時吃飯。

相關文章
相關標籤/搜索