Vue.js源碼學(xué)習(xí)二 —— 生命周期 LifeCycle 學(xué)習(xí)

春節(jié)繼續(xù)寫博客~加油!

這次來學(xué)習(xí)一下Vue的生命周期揩魂,看看生命周期是怎么回事。

callHook

生命周期主要就是在源碼某個(gè)時(shí)間點(diǎn)執(zhí)行這個(gè) callHook 方法來調(diào)用 vm.$options 的生命周期鉤子方法(如果定義了生命周期鉤子方法的話)。
我們來看看 callHook 代碼:

export function callHook (vm: Component, hook: string) {
  const handlers = vm.$options[hook] // 獲取Vue選項(xiàng)中的生命周期鉤子函數(shù)
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm) // 執(zhí)行生命周期函數(shù)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
}

比如觸發(fā) mounted 鉤子的方法:

callHook(vm, 'mounted')

生命周期鉤子

先上一張圖看下Vue的生命周期,我們可以在相應(yīng)的生命周期中定義一些事件肩狂。


Vue生命周期

beforeCreate & created

先看看這兩個(gè)方法調(diào)用的時(shí)間。

beforeCreate
在實(shí)例初始化之后姥饰,數(shù)據(jù)觀測 (data observer) 和 event/watcher 事件配置之前被調(diào)用傻谁。
created
在實(shí)例創(chuàng)建完成后被立即調(diào)用。在這一步列粪,實(shí)例已完成以下的配置:數(shù)據(jù)觀測 (data observer)栅螟,屬性和方法的運(yùn)算,watch/event 事件回調(diào)篱竭。然而,掛載階段還沒開始步绸,$el 屬性目前不可見掺逼。

具體代碼如下

  // src/core/instance/init.js
  Vue.prototype._init = function (options?: Object) {
    ……
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件
    initRender(vm) // 初始化渲染
    callHook(vm, 'beforeCreate')
    initInjections(vm) // 初始化Inject
    initState(vm) // 初始化數(shù)據(jù)
    initProvide(vm) // 初始化Provide
    callHook(vm, 'created')
    ……
    if (vm.$options.el) {
      vm.$mount(vm.$options.el) // 如果有el屬性,將內(nèi)容掛載到el中去瓤介。
    }
  }

beforeMount & mounted

beforeMount
在掛載開始之前被調(diào)用:相關(guān)的 render 函數(shù)首次被調(diào)用吕喘。該鉤子在服務(wù)器端渲染期間不被調(diào)用赘那。
mounted
el 被新創(chuàng)建的 vm.$el 替換,并掛載到實(shí)例上去之后調(diào)用該鉤子氯质。如果 root 實(shí)例掛載了一個(gè)文檔內(nèi)元素募舟,當(dāng) mounted 被調(diào)用時(shí) vm.$el 也在文檔內(nèi)。

貼出代碼邏輯

// src/core/instance/lifecycle.js
// 掛載組件的方法
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

那么這個(gè) mountComponent 在哪里用了呢闻察?就是在Vue的 $mount 方法中使用拱礁。

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

最后會在Vue初始化的時(shí)候,判斷是否有 el辕漂,如果有則執(zhí)行 $mount 方法呢灶。

// src/core/instance/init.js
if (vm.$options.el) {
  vm.$mount(vm.$options.el) // 如果有el屬性,將內(nèi)容掛載到el中去钉嘹。
}

至此生命周期邏輯應(yīng)該是 beforeCreate - created - beforeMount -mounted

beforeUpdate & updated

beforeUpdate
數(shù)據(jù)更新時(shí)調(diào)用鸯乃,發(fā)生在虛擬 DOM 打補(bǔ)丁之前。這里適合在更新之前訪問現(xiàn)有的 DOM跋涣,比如手動移除已添加的事件監(jiān)聽器缨睡。
updated
由于數(shù)據(jù)更改導(dǎo)致的虛擬 DOM 重新渲染和打補(bǔ)丁,在這之后會調(diào)用該鉤子陈辱。

找代碼邏輯~ beforeUpdate 和 updated 在兩個(gè)地方調(diào)用奖年。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    // 如果是已經(jīng)掛載的,就觸發(fā)beforeUpdate方法性置。
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    ……
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

在執(zhí)行 _update 方法的時(shí)候拾并,如果 DOM 已經(jīng)掛載了,則調(diào)用 beforeUpdate 方法鹏浅。
在 _update 方法的最后作者也注視了調(diào)用 updated hook 的位置:updated 鉤子由 scheduler 調(diào)用來確保子組件在一個(gè)父組件的 update 鉤子中嗅义。
我們找到 scheduler,發(fā)現(xiàn)有個(gè) callUpdateHooks 方法隐砸,該方法遍歷了 watcher 數(shù)組之碗。

// src/core/observer/scheduler.js
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

這個(gè) callUpdatedHooksflushSchedulerQueue 方法中調(diào)用。

/**
 * 刷新隊(duì)列并運(yùn)行watcher
 */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // 調(diào)用組件的updated和activated生命周期
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

繼續(xù)找下去

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true // 此參數(shù)用于判斷watcher的ID是否存在
    ……
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

最終在 watcher.js 找到 update 方法:

  // src/core/observer/watcher.js
  update () {
    // lazy 懶加載
    // sync 組件數(shù)據(jù)雙向改變
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this) // 排隊(duì)watcher
    }
  }

等于是隊(duì)列執(zhí)行完 Watcher 數(shù)組的 update 方法后調(diào)用了 updated 鉤子函數(shù)季希。

beforeDestroy & destroyed

beforeDestroy
實(shí)例銷毀之前調(diào)用褪那。在這一步,實(shí)例仍然完全可用式塌。該鉤子在服務(wù)器端渲染期間不被調(diào)用博敬。
destroyed
Vue 實(shí)例銷毀后調(diào)用。調(diào)用后峰尝,Vue 實(shí)例指示的所有東西都會解綁定偏窝,所有的事件監(jiān)聽器會被移除,所有的子實(shí)例也會被銷毀。該鉤子在服務(wù)器端渲染期間不被調(diào)用祭往。

看代碼~

  // src/core/instance/lifecycle.js
  // 銷毀方法
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      // 已經(jīng)被銷毀
      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)
    // 觸發(fā) destroyed 鉤子
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
  }

這是一個(gè)銷毀 Vue 實(shí)例的過程伦意,將各種配置清空和移除。

activated & deactivated

activated
keep-alive 組件激活時(shí)調(diào)用硼补。
deactivated
keep-alive 組件停用時(shí)調(diào)用驮肉。

找到實(shí)現(xiàn)代碼的地方

// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  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')
  }
}

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}

以上兩個(gè)方法關(guān)鍵就是修改了 vm._inactive 的值,并且鄉(xiāng)下遍歷子組件已骇,最后觸發(fā)鉤子方法离钝。

errorCaptured

當(dāng)捕獲一個(gè)來自子孫組件的錯(cuò)誤時(shí)被調(diào)用。此鉤子會收到三個(gè)參數(shù):錯(cuò)誤對象疾捍、發(fā)生錯(cuò)誤的組件實(shí)例以及一個(gè)包含錯(cuò)誤來源信息的字符串奈辰。此鉤子可以返回 false 以阻止該錯(cuò)誤繼續(xù)向上傳播。

這是 2.5 以上版本有的一個(gè)鉤子乱豆,用于處理錯(cuò)誤奖恰。

// src/core/util/error.js
export function handleError (err: Error, vm: any, info: string) {
  if (vm) {
    let cur = vm
    // 向上冒泡遍歷
    while ((cur = cur.$parent)) {
      // 獲取鉤子函數(shù)
      const hooks = cur.$options.errorCaptured
      if (hooks) {
        for (let i = 0; i < hooks.length; i++) {
          try {
            // 執(zhí)行 errorCaptured 鉤子函數(shù)
            const capture = hooks[i].call(cur, err, vm, info) === false
            if (capture) return
          } catch (e) {
            globalHandleError(e, cur, 'errorCaptured hook')
          }
        }
      }
    }
  }
  globalHandleError(err, vm, info)
}

代碼很簡單,看代碼即可~

生命周期

除了生命周期鉤子外宛裕,vue還提供了生命周期方法來直接調(diào)用瑟啃。

vm.$mount

如果 Vue 實(shí)例在實(shí)例化時(shí)沒有收到 el 選項(xiàng),則它處于“未掛載”狀態(tài)揩尸,沒有關(guān)聯(lián)的 DOM 元素蛹屿。可以使用 vm.$mount() 手動地掛載一個(gè)未掛載的實(shí)例岩榆。
如果沒有提供 elementOrSelector 參數(shù)错负,模板將被渲染為文檔之外的的元素,并且你必須使用原生 DOM API 把它插入文檔中勇边。
這個(gè)方法返回實(shí)例自身犹撒,因而可以鏈?zhǔn)秸{(diào)用其它實(shí)例方法。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  if (el === document.body || el === document.documentElement) {
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    // 獲取template
    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)
    }
    // 編譯template
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 執(zhí)行 $mount 方法
  return mount.call(this, el, hydrating)
}

其實(shí)很簡單粒褒,先獲取html代碼识颊,然后執(zhí)行 compileToFunctions 方法執(zhí)行編譯過程(具體編譯過程在學(xué)習(xí)Render的時(shí)候再說)。

vm.$forceUpdate

迫使 Vue 實(shí)例重新渲染奕坟。注意它僅僅影響實(shí)例本身和插入插槽內(nèi)容的子組件祥款,而不是所有子組件。

   Vue.prototype.$forceUpdate = function () {
    var vm = this;
    if (vm._watcher) {
      vm._watcher.update();
    }
  };

這是強(qiáng)制更新方法月杉,執(zhí)行了 vm._watcher.update() 方法刃跛。

vm.$nextTick

將回調(diào)延遲到下次 DOM 更新循環(huán)之后執(zhí)行。在修改數(shù)據(jù)之后立即使用它苛萎,然后等待 DOM 更新奠伪。它跟全局方法 Vue.nextTick 一樣跌帐,不同的是回調(diào)的 this 自動綁定到調(diào)用它的實(shí)例上。

找了找 vm.$nextTick 的代碼

  // src/core/instance/render.js
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

找到這個(gè) nextTick 方法:

// src/core/util/next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

具體功能邏輯等學(xué)習(xí)完 render 再更新……

vm.$destroy

完全銷毀一個(gè)實(shí)例绊率。清理它與其它實(shí)例的連接,解綁它的全部指令及事件監(jiān)聽器究履。
觸發(fā) beforeDestroy 和 destroyed 的鉤子滤否。

關(guān)于$destroy 我們之前再說 destroyed 鉤子的時(shí)候提到過了,這里就不再贅述最仑。

  Vue.prototype.$destroy = function () {
    ……
  }

最后

首先說下過年博客計(jì)劃藐俺,過年學(xué)習(xí)Vue各個(gè)模塊的源碼,并發(fā)布相應(yīng)博客泥彤。另外還會發(fā)布一些前端知識的整理欲芹,便于下個(gè)月找工作~
然后,小結(jié)下自己看源碼的一些小技巧:

  • 重點(diǎn)關(guān)注方法的執(zhí)行吟吝、對象的實(shí)例化菱父、對象屬性的修改。
  • 忽略開發(fā)版本提示邏輯剑逃、內(nèi)部變量賦值浙宜。
  • 有目標(biāo)的看代碼,根據(jù)主線目標(biāo)進(jìn)行源碼學(xué)習(xí)蛹磺。

OK粟瞬,今天就這么多~ 明天去學(xué)習(xí)下Vue的事件源碼!加油萤捆!明天見裙品!

Vue.js學(xué)習(xí)系列

鑒于前端知識碎片化嚴(yán)重,我希望能夠系統(tǒng)化的整理出一套關(guān)于Vue的學(xué)習(xí)系列博客俗或。

Vue.js學(xué)習(xí)系列項(xiàng)目地址

本文源碼已收入到GitHub中市怎,以供參考,當(dāng)然能留下一個(gè)star更好啦-蕴侣。
https://github.com/violetjack/VueStudyDemos

關(guān)于作者

VioletJack焰轻,高效學(xué)習(xí)前端工程師,喜歡研究提高效率的方法昆雀,也專注于Vue前端相關(guān)知識的學(xué)習(xí)辱志、整理。
歡迎關(guān)注狞膘、點(diǎn)贊揩懒、評論留言~我將持續(xù)產(chǎn)出Vue相關(guān)優(yōu)質(zhì)內(nèi)容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571d953d39b0570068145cd1
CSDN: http://blog.csdn.net/violetjack0808
簡書: http://www.reibang.com/users/54ae4af3a98d/latest_articles
Github: https://github.com/violetjack

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挽封,一起剝皮案震驚了整個(gè)濱河市已球,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖智亮,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忆某,死亡現(xiàn)場離奇詭異,居然都是意外死亡阔蛉,警方通過查閱死者的電腦和手機(jī)弃舒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來状原,“玉大人聋呢,你說我怎么就攤上這事〉咔” “怎么了削锰?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長毕莱。 經(jīng)常有香客問我器贩,道長,這世上最難降的妖魔是什么央串? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任磨澡,我火速辦了婚禮,結(jié)果婚禮上质和,老公的妹妹穿的比我還像新娘稳摄。我一直安慰自己,他們只是感情好饲宿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布厦酬。 她就那樣靜靜地躺著,像睡著了一般瘫想。 火紅的嫁衣襯著肌膚如雪仗阅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天国夜,我揣著相機(jī)與錄音减噪,去河邊找鬼。 笑死车吹,一個(gè)胖子當(dāng)著我的面吹牛筹裕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窄驹,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼朝卒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了乐埠?” 一聲冷哼從身側(cè)響起抗斤,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤囚企,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后瑞眼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體龙宏,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年伤疙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了烦衣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸵赫。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绸狐,死狀恐怖星立,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厨姚,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布键菱,位于F島的核電站谬墙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏经备。R本人自食惡果不足惜拭抬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侵蒙。 院中可真熱鬧造虎,春花似錦、人聲如沸纷闺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽犁功。三九已至氓轰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浸卦,已是汗流浹背署鸡。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留限嫌,地道東北人靴庆。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像萤皂,于是被迫代替她去往敵國和親撒穷。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容