Vue2.0源碼閱讀筆記(十二):生命週期

  在 Vue 中,除函數式組件外,全部組件都是 Vue 實例。每一個 Vue 實例在被建立時都要通過一系列的初始化過程:數據監聽、編譯模板、將實例掛載到 DOM 並在數據變化時更新 DOM 等。
  在生成 Vue 實例的過程當中會運行一些叫作生命週期鉤子的函數,這給了用戶在不一樣階段添加本身的代碼的機會。本文從源碼的角度來詳細闡述組件生命週期的相關內容。
前端

1、鉤子函數的調用

  生命週期鉤子函數調用是經過 callHook 函數完成的,callHook 函數主要包含三個方面的內容。
vue

function callHook (vm, hook) {
  pushTarget()
  const handlers = vm.$options[hook]
  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()
}
複製代碼

一、調用鉤子函數

  在生成 Vue 實例的過程當中會調用 mergeOptions 函數對選項進行處理,生命週期鉤子函數通過合併處理後會添加到實例對象的 $options 屬性上,合併後各生命週期函數存儲在對應的數組中。具體細節可參看文章《選項合併》
  callHook 函數調用的形式以下所示:
node

// 調用 created 生命週期鉤子函數
callHook(vm, 'created')
複製代碼

  此時 callHook 函數會循環遍歷執行 vm.$options.created 數組中的函數,以完成 created 生命週期鉤子函數的調用。
vue-router

二、防止收集冗餘依賴

  在函數首尾有以下代碼:
後端

function callHook (vm, hook) {
  pushTarget()
  /* 省略... */
  popTarget()
}
複製代碼

  這兩個函數的源碼以下所示:
數組

Dep.target = null
const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
複製代碼

  Vue 實例的當前觀察者對象是惟一的,所謂當前觀察者對象是指即將要收集依賴的目標,pushTarget 函數將觀察者對象入棧而不是簡單的賦值,是爲了在當前觀察者對象操做完成後恢復成以前的觀察者對象。
  在函數的首尾調用 pushTarget() 和 popTarget() 函數,是爲了防止在生命週期鉤子函數中使用 props 數據時收集冗餘的依賴。具體詳情可參看《響應式原理》
緩存

三、hookEvent

  在 callHook 函數中還有一部分代碼:
ide

if (vm._hasHookEvent) {
  vm.$emit('hook:' + hook)
}
複製代碼

  這行代碼比較有意思,也就是說在執行生命週期鉤子函數時,若是 vm._hasHookEvent 的值爲 true,則會額外觸發一個形如 hook:created 的事件。
  那麼何時實例的 _hasHookEvent 屬性值爲真呢?還記得在上篇文章講解 $on 方式時有提過這點:
函數

const hookRE = /^hook:/
Vue.prototype.$on = function(event, fn){
  /* 省略... */
  if (hookRE.test(event)) {
    vm._hasHookEvent = true
  }
  /* 省略... */
}
複製代碼

  上篇文章同時講到,在組件上使用自定義指令最終會轉化成調用 $on 的形式,也就是說按照如下使用就能命中這種狀況:
oop

<Child @hook:created = "doSomething"></Child>
複製代碼

  這種形式的事件稱爲 hookEvent,在官方文檔上沒有找到 hookEvent 的說明,可是在 Vue 源碼中有實現。所謂 hookEvent 就是特殊命名的事件—— hook: + 生命週期名稱。這種事件會在子組件對應生命週期鉤子函數調用時被調用。
  那 hookEvent 有什麼用呢?其實在使用第三方組件的時候可以用到,使用 hookEvent 能夠在不破壞第三方組件代碼的前提下,向其注入生命週期函數。

2、組件的生命週期

  關於組件實例的生命週期,官網上面有一張很經典的圖片:

  這張圖片包含的信息較多,下面咱們經過拆解這張圖片來逐步講解組件實例的生命週期。

一、beforeCreate和created

  Vue 的構造函數主要包含 _init 方法,在組件實例化的過程當中會經過該函數完成一系列初始化操做。

function Vue (options) {
  /* 省略警告信息 */
  this._init(options)
}
複製代碼

  _init 方法首先進行合併選項,而後初始化生命週期、事件等,最後掛載 DOM 元素。代碼以下所示:

Vue.prototype._init = function (options) {
  const vm = this
  /*...*/
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  /*...*/
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  /*...*/
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
複製代碼

  這裏函數調用的順序很重要,數據的處理都是在 beforeCreate 生命週期函數調用以後初始化的,也就是說在 beforeCreate 生命週期函數中,不能使用 props、methods、data、computed 和 watch 等數據,也不能使用 provide/inject 中的數據。通常從後端加載數據不用賦值給data中時,能夠放在這個生命週期中。
  在 beforeCreate 與 created 生命週期函數調用中間,調用初始化各個數據的函數。initState 函數代碼以下所示:

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)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製代碼

  注意 initState 函數中函數的調用順序:initProps——initData——initComputed——initWatch。這樣初始化順序的結果是在 data 選項中可使用 props;在 computed 選項中可使用 data、props 中的數據;watch 選項能夠監聽 data、props、computed 數據的變化。methods 選項的組成是函數,在函數調用時這些初始化工做已經完成,因此可使用所有的數據。
  初始化 inject 的 initInjections 函數在 initState 以前調用,最後調用初始化 provide 的 initProvide 函數。這樣就決定了在 data、props、computed 等選項中可使用 inject 中的數據,provide 選項中可使用 data、props、computed、inject 等的數據。
  調用 created 生命週期函數以前,數據初始化已經完成,在函數中能夠操做這些數據。向後端請求的數據須要賦值給 data 時,能夠放在 created 生命週期函數中。

二、beforeMount和mounted

  在 _init 函數的最後執行 $mount 方法來完成DOM掛載,下面以 運行時+編譯器的版原本闡述具體掛載過程。
  編譯器相關代碼以下所示:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el,hydrating){
  el = el && query(el)
  /* 省略... */
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* 省略... */
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        /* 省略... */
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* 省略... */ 
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      /* 省略... */
    }
  }
  return mount.call(this, el, hydrating)
}
複製代碼

  該函數的做用是將 template/el 轉化成渲染函數,具體的轉化過程可參看《模板編譯》一文。
  根據渲染函數完成掛載的代碼以下所示:

Vue.prototype.$mount = function (el,hydrating){
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

function mountComponent (vm,el,hydrating){
  vm.$el = el
  /* 省略渲染函數不存在的警告信息 */
  callHook(vm, 'beforeMount')

  let updateComponent
  /* 刪除性能埋點相關 */
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製代碼

  能夠看到 beforeMount 生命週期是在渲染函數生成以後、開始執行掛載以前調用的。beforeMount 鉤子函數執行後,實例化一個渲染函數觀察者對象,關於 Watcher 相關內容能夠參看《響應式原理》
  從渲染函數到生成真實DOM的過程由 updateComponent 函數來完成,其中 _render 函數的做用是根據渲染函數生成 VNode,_update 函數的做用是根據 VNode 生成真實DOM並插入到對應位置中。
  在掛載完成後,會調用 mounted 生命週期鉤子函數,在該生命週期內能夠對DOM進行操做。
  這裏有個判斷條件:vm.$vnode == null,組件初始化的時候 $vnode 不爲空,當條件成立時,說明是經過 new Vue() 來進行初始化的。換而言之,組件初始化時,不會在此處執行 mounted 生命週期鉤子函數,那麼組件 mounted 生命週期函數在何處調用呢?
  _update 函數本質上是經過調用 patch 函數來完成真實DOM元素的生成與插入,在 patch 函數的最後有以下代碼:

function patch (oldVnode,vnode,hydrating,removeOnly){
  /* 省略... */
  invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch)
  return vnode.elm
}

function invokeInsertHook (vnode, queue, initial) {
  /* 省略... */
  for (let i = 0; i < queue.length; ++i) {
    queue[i].data.hook.insert(queue[i])
  }
}

function insert (vnode) {
  const { context, componentInstance } = vnode
  if (!componentInstance._isMounted) {
    componentInstance._isMounted = true
    callHook(componentInstance, 'mounted')
  }
  /* 省略 keep-alive 相關...*/
}
複製代碼

  能夠看到組件 mounted 生命週期鉤子函數的調用是在 patch 的最後階段進行的,另外 insertedVnodeQueue 是一個 VNode 數組,數組中 VNode 的順序是子 VNode 在前,父 VNode 在後,所以 mounted 鉤子函數的執行順序也是子組件先執行,父組件後執行。

三、beforeUpdate和updated

  beforeUpdate 與 updated 兩個生命週期跟數據更新有關,數據響應式原理在本系列文章的第二篇已經詳細闡述過,這裏只說跟生命週期有關的部分。
  上一小節提到,在實例掛載過程當中有以下代碼:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true)
複製代碼

  Watcher 構造函數代碼以下所示:

class Watcher {
  constructor (vm,expOrFn,cb,options,isRenderWatcher) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      /* 省略... */
      this.before = options.before
    }
  }
  /* 省略... */
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  /* 省略... */
}
複製代碼

  能夠看到在實例化渲染函數觀察者對象時,會將傳入的 before 函數添加到觀察者對象上。在數據更新時會執行 update 方法,在沒有添增強制要求時,默認執行 queueWatcher 函數完成數據更新。

export function queueWatcher (watcher: Watcher) {
  /* 省略... */
  flushSchedulerQueue()
  /* 省略... */
}

function flushSchedulerQueue () {
  /* 省略... */
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    /* 省略... */
  }
  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')
    }
  }
}
複製代碼

  數據更新是經過觀察者對象的實例方法 run 完成的,從上代碼能夠看到:在數據更新前會調用實例對象上的 before 方法,從而執行 beforeUpdate 生命週期鉤子函數;在數據更新完成後,經過執行 callUpdatedHooks 函數完成 updated 生命週期函數的調用。

四、beforeDestroy和destroyed

  調用實例方法 $destroy() 徹底銷燬一個實例。清理它與其它實例的鏈接,解綁它的所有指令及事件監聽器。beforeDestroy 與 destroyed 生命週期函數就是在這期間被調用的。

Vue.prototype.$destroy = function () {
  const vm = this
  if (vm._isBeingDestroyed) { return }
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract){
    remove(parent.$children, vm)
  }
  if (vm._watcher){
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--){
    vm._watchers[i].teardown()
  }
  if (vm._data.__ob__){
    vm._data.__ob__.vmCount--
  }
  vm._isDestroyed = true
  vm.__patch__(vm._vnode, null)
  callHook(vm, 'destroyed')
  vm.$off()
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}
複製代碼

  首先判斷實例上 _isBeingDestroyed 是否爲 true,這是實例正在被銷燬的標識,爲了防止重複銷燬組件。當正式開始執行銷燬邏輯以前,調用 beforeDestroy 生命週期鉤子函數。
  銷燬組件的具體步驟有:

一、將實例從其父級實例中刪除。
二、移除實例的依賴。
三、移除實例內響應式數據的引用。
四、刪除子組件實例。

  完成上述操做後調用 destroyed 生命週期鉤子函數,而後移除實例上的所有事件監聽器。

3、keep-alive組件

  當組件被 keep-alive 內置組件包裹時,組件實例會被緩存起來。這些組件在首次渲染時各生命週期與普通組件同樣,再次渲染時 created、mounted 等鉤子函數就再也不生效。
  被 keep-alive 包裹的組件被緩存以後有兩個獨有的生命週期: activated 和 deactivated。activated 生命週期在組件激活時調用、deactivated 生命週期在組件停用時調用。
  上一節講 mounted 生命週期時說過,組件的 mounted 的生命週期鉤子函數是在 insert 方法中調用的。當時將函數中對 keep-alive 的處理省略了,這裏重點闡述。

insert (vnode) {
  /* 省略... */
  if (vnode.data.keepAlive) {
    if (context._isMounted) {
      queueActivatedComponent(componentInstance)
    } else {
      activateChildComponent(componentInstance, true)
    }
  }
}
複製代碼

  queueActivatedComponent 函數的調用是爲了修復 vue-router 中的一個問題:在更新過程當中 keep-alive 的子組件可能會發生改變,直接遍歷樹結構可能會調用錯誤子組件實例的 activated 生命週期鉤子函數,所以這裏不作處理而是將組件實例放入隊列中,等 patch 過程結束後再作處理。
  queueActivatedComponent 最終也是調用 activateChildComponent 函數來執行 activated 生命週期鉤子函數。

function activateChildComponent(vm,direct){
  /* 省略... */
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}
複製代碼

  能夠看到就是在這裏調用的 activated 生命週期鉤子函數,而且會遞歸調用所有子組件的 activated 生命週期鉤子函數。
  deactivated 生命週期是在 destroy 鉤子函數中調用的:

destroy (vnode) {
  const { componentInstance } = vnode
  if (!componentInstance._isDestroyed) {
    if (!vnode.data.keepAlive) {
      componentInstance.$destroy()
    } else {
      deactivateChildComponent(componentInstance, true)
    }
  }
}
複製代碼

  keep-alive 的子組件的子組件會走 else 分支,直接調用 deactivateChildComponent 函數。

function deactivateChildComponent(vm, direct){
  /* 省略... */
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}
複製代碼

  在該函數中,會調用的 deactivated 生命週期鉤子函數,而且會遞歸調用所有子組件的 deactivated 生命週期鉤子函數。

4、總結

  生命週期鉤子函數的是經過 callHook 來調用的,該函數不只遍歷執行對應的生命週期函數,還能防止收集冗餘依賴和觸發 hookEvent 事件。hookEvent 可以非侵入的向一個組件注入生命週期函數。
  經過 new Vue() 實例化 Vue 對象會調用 _init 方法完成一系列初始化操做,在初始化數據以前會調用 beforeCreate 鉤子,在數據初始化後調用 created 鉤子。在生成渲染函數以後,調用 beforeMount 鉤子,接着根據渲染函數生成真實DOM並掛載,而後調用 mounted 鉤子。數據更新時,在從新渲染以前調用 beforeUpdate 鉤子,在完成渲染後調用 updated 鉤子。在調用實例方法 $destroy() 銷燬實例時首先調用 beforeDestroy 鉤子,而後執行銷燬操做,最後調用 destroyed 鉤子。
  被 keep-alive 緩存起來的組件被激活時會調用 activated 鉤子,在 patch 最後階段的 insert 鉤子函數中執行。組件停用時調用 deactivated 鉤子,在 patch 的 destroy 鉤子函數中執行。

歡迎關注公衆號:前端桃花源,互相交流學習!

相關文章
相關標籤/搜索