春節(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)的生命周期中定義一些事件肩狂。
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è) callUpdatedHooks
在 flushSchedulerQueue
方法中調(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