再談Vue的生命週期----結合Vue源碼

簡介

關於Vue的生命週期函數,目前網上有許多介紹文章,但也都只是分析了表象。這篇文檔,將結合Vue源碼分析,爲何會有這樣的表象。javascript

Vue中的生命週期函數也能夠稱之爲生命週期鉤子(hook)函數,在特定的時期,調用特定的函數。html

隨着項目需求的不斷擴大,生命週期函數被普遍使用在數據初始化、回收、改變Loading狀態、發起異步請求等各個方面。vue

而Vue實例的生命週期函數有beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestpry、destroyed,8個。java

本文假設讀者使用過Vue.js,但對相應的開發經驗不作要求。若是你對Vue很感興趣,殊不知如何下手,建議你先閱讀官方文本node

資源

如下是這篇文章所需的資源,歡迎下載。git

  1. 項目倉庫
  2. Vue源碼筆記

表象

咱們在該頁面來研究,Vue的生命週期函數究竟會在什麼時候調用,又會有什麼不一樣的特性。強烈建議你直接將項目倉庫克隆至本地,並在真機環境中,跑一跑。Vue.js已經添加在版本庫中,所以你不須要添加任何依賴,直接在瀏覽器中打開lifeCycle.html便可。github

$ git clone https://github.com/AmberAAA/vue-guide

編寫實現組件

定義templatedata,使其在能夠在屏幕實時渲染出來。數組

{
  //...
  template: `
    <div>
      <p v-html="info"></p>
    </div>
  `,
  data () {
    return {
      info: ''
    }
  }
  //...
}

beforeCreate爲例,定義所有的證實周期函數。瀏覽器

beforeCreate() {
    console.group('------beforeCreate------');
    console.log('beforeCreate called')
    console.log(this)
    console.log(this.$data)
    console.log(this.$el)
    this.info += 'beforeCreate called <br>'
    console.groupEnd();
  }

屏幕輸出

在瀏覽器中打開lifeCycle.html,點擊掛載組件後,屏幕依次輸出created called beforeMount called mounted called 。表現出,在掛載組件後,infocreated, beforeMount, mounted賦值,並渲染至屏幕上。可是本應在最開始就執行的beforeCreate卻並無給info賦值。
卸載組件時,由於程序運行太快,爲了方便觀察,特地爲beforeDestroybeforeDestroy函數在最後添加了斷點。發現點擊卸載組價後,Vue在v-if=true時會直接從文檔模型中卸載組件(此時組件已經不在document)。異步

控制檯輸出

控制檯輸出的內容很是具備表明性。

1539056706.png

咱們能夠發現,控制檯按照建立、掛載、更新、銷燬依次打印出對應的鉤子函數。展開來看

圖片描述

在觸發beforeCreate函數時,vue實例還還沒有初始化$data,所以也就沒法給$data賦值,也就很好的解釋了爲何在屏幕上,沒有渲染出beforeCreate called。同時,由於還沒有被掛載,也就沒法獲取到$el

在觸發created函數時,其實就代表,該組件已經被建立了。所以給info賦值後,待組件掛載後,視圖也會渲染出created called

在觸發beforeMount函數時,其實就代表,該組件即將被掛載。此時組建表現出的特性與created保持一致。

在觸發mounted函數時,其實就代表,該組件已經被掛載。所以給info賦值後,待組件掛載後,視圖也會渲染出mounted called,而且控制檯能夠獲取到$el

觸發beforeUpdateupdated,分別表示視圖更新先後,更新前$data領先視圖,更新後,保持一致。在這兩個回調函數中,更改data時注意避免循環回調。

觸發beforeDestroydestroyed,表示實例在銷燬先後,改變$data,會觸發一次updated,因在同一個函數中(下文會介紹)回調,故捏合在一塊兒說明。

名稱 觸發階段 $data $el
beforeCreate 組件建立前
created 組件建立後
beforeMount 組件掛載前
mounted 組件掛載後
beforeUpdate 組件更新前
updated 組件更新後
beforeDestroy 組件建立前
destroyed 組件建立前

原理

Vue生命週期函數在源碼文件/src/core/instance/init.js中定義,並在/src/core/instance/init.jssrc/core/instance/lifecycle.js/src/core/observer/scheduler.js三個文件中調用了全部的生命週期函數

callHooK

當在特定的使其,須要調用生命週期鉤子時,源碼只需調用callHook函數,並傳入兩個參數,第一個爲vue實例,第二個爲鉤子名稱。以下

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    //? 這裏爲何是數組?在什麼狀況下,數組的索引會大於1?
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

耍個流氓

callHook在打包時,並無暴露在全局做用域。但咱們能夠根據Vue實例來手動調用生命週期函數。試着在掛在組件後在控制檯輸入vue.$children[0].$options['beforeCreate'][0].call(vue.$children[0]),能夠發現組件的beforeCreate鉤子已經被觸發了。而且表示出了與本意相駁的特性。此時由於組件已經初始化,而且已經掛載,因此成功在控制檯打印出$el$data,並在修改info後成功觸發了beforeUpdatebeforeUpdate

beforeCreatecreated

Vue會在/src/core/instance/init.js中經過initMixin函數對Vue實例進行進一步初始化操做。

export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) {
      /*
        ....
      */
      vm._self = vm
      initLifecycle(vm) 
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)   // 定義$data
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')

      /*
        ...
      */
    }
  }

能夠看出在執行callHook(vm, 'beforeCreate')以前,Vue還還沒有初始化data,這也就解釋了,爲何在控制檯beforeCreate獲取到的$dataundefined,而callHook(vm, 'created')卻能夠,以及屏幕上爲何沒有打印出beforeCreate called

beforeMountmounted

Vue在/src/core/instance/lifecycle.js中定義了mountComponent函數,並在該函數內,調用了beforeMountmounted

export function mountComponent (
    vm: Component,
    el: ?Element,
    hydrating?: boolean
  ): Component {
    vm.$el = el    // 組件掛載時 `el` 爲`undefined`

    callHook(vm, 'beforeMount') // 因此獲取到的`$el`爲`undefined`

    /*
      ...
    */
    // 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

    //! 挖個新坑 下節分享渲染watch。 通過渲染後,便可獲取`$el`
    new Watcher(vm, updateComponent, noop, null, 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
      // 由於已經渲染,`$el`此時已經能夠成功獲取
      callHook(vm, 'mounted')
    }
    return vm
  }

beforeUpdateupdated

beforeUpdateupdated涉及到watcher,所以將會在之後的章節進行詳解。

beforeDestroydestroyed

Vue將卸載組件的方法直接定義在原型鏈上,所以能夠經過直接調用vm.$destroy()方法來卸載組件。

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    // 吊起`beforeDestroy`鉤子函數
    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
    // 調起`destroyed`鉤子函數
    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
    }
  }
}

問題

  1. vue.$children[0].$options['beforeCreate']爲何是一個數組?
  2. 卸載組件,會觸發一個updated called?
  3. new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)這行代碼以後發生了什麼?
  4. beforeUpdate背後實現原理。
相關文章
相關標籤/搜索