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

觀感度:🌟🌟🌟🌟🌟

口味:蜜桃烏龍

烹飪時間:30min

在我們的實際項目中,與Vue的生命週期打交道可以說是家常便飯。掌握Vue的生命週期對開發者來說是特別重要的。那麼如果能夠從源碼角度理解Vue的生命週期,對我們的開發和成長會有進一步的提升。

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

18.jpg

Vue的生命週期

大自然有春夏秋冬,人有生老病死,優秀的Vue當然也存在自己的生命週期。

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

生命週期函數

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

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

鉤子函數

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

19.jpg

此hook非彼hook。

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

我在官方提供的圖片上添加了相關注釋,希望能夠讓大家看的更明白一些,如下圖。

20.png

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

總的來說,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技術揭祕

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

// 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指向問題請移步我的另一篇文章如何治療JavaScript中的this

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

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 以阻止該錯誤繼續向上傳播

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

交流

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

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

gongzhonghao.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章