vue 生命周期_深入理解Vue实例生命周期
?vue實例生命周期與生命周期鉤子?
每個 Vue 實
例在被創建時都會經過一系列的初始化過程。例如,需要設置數據監聽、編譯模板、將實例掛載到 DOM 并在數據變化時更新 DOM 等。為了讓開發者在Vue實例生命周期的不同階段有機會插入自己的代碼邏輯,vue提供了一種叫做生命周期鉤子的函數。主要的生命周期鉤子如下:
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeDestroy
destroyed
官方提供的vue實例生命周期示意圖很好地說明了整個過程。大家可以參考這張圖來閱讀本文。?
初始化
執行new Vue()后,將進入實例初始化階段。這個階段會觸發兩個鉤子:beforeCreate?和?created??聪轮饕a:
// Vue構造函數function Vue (options) {
// 實例初始化
this._init(options)
}
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid 為每個vue實例分配一個唯一的id
vm._uid = uid++
vm._isVue = true
//...省略部分代碼
if (options && options._isComponent) {
// 合并組件選項對象
initInternalComponent(vm, options)
} else {
// 合并vue選項對象
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
// 初始化完成后,調用created鉤子函數
callHook(vm, 'created')
//...省略部分代碼
// 如果指定了掛載元素,則執行掛載邏輯
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
beforeCreate
從_init方法中可以看到,vue會將用戶所定義的選項對象與vue構造函數上所預先定義的靜態選項對象進行合并。
調用initLifecycle函數為實例初始化生命周期有關的屬性。例如初始化vue實例_isMounted、_isDestroyed、_inactive等屬性的值。
調用?initEvents?函數為實例初始化事件相關的屬性。例如初始化vue實例的_events、_hasHookEvent等屬性的值。
調用initRender函數為實例初始化渲染相關的屬性。例如初始化vue實例的?_vnode、_c()、$slots等屬性的值。
由此可知,beforeCreate鉤子被調用時,已經初始化完成了lifecycle、render、event;但尚未處理data、props、methods、provide/inject、watch、computed等。
created
從代碼中能看到,created?鉤子被調用時,先后執行了initInjections、initState和?initProvide。這幾個函數主要是將data、props、inject、methods、computed、watch中所定義的屬性使用defineProperty代理到vue實例上。同時會把data、props、inject、computed、watch中的屬性全部轉換成響應式的。經過這些處理,后面當我們在實例中修改屬性值的時候,就會自動觸發頁面的重繪了。
由此可知,created鉤子被調用時,完成了實例的初始化,實例屬性也具備了響應式的能力。但尚未開始DOM元素的掛載。一般在鉤子里常見的操作是異步向后端獲取數據。
DOM 元素掛載
實例初始化完成后,會調用$mount開始DOM元素掛載。這個階段會觸發兩個鉤子函數:beforeMount和mounted。
// src/platforms/web/entry-runtime-with-compiler.jsVue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src/platforms/web/runtime/index.js
// 覆寫$mount方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
if (!options.render) {
let template = options.template
// 選項中指定了template
if (template) {
if (typeof template === 'string') {
// 如果值以 # 開始,則它將被用作選擇符,并使用匹配元素的 innerHTML 作為模板
if (template.charAt(0) === '#') {
template = idToTemplate(template)
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// 雖然在文檔中并未說明,但template還可以指定一個DOM元素作為模板
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 選項中指定了el
template = getOuterHTML(el)
}
// 將模板解析成render函數
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)
}
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 將對DOM元素的引用保存到$el
vm.$el = el
// ...省略部分代碼
// 調用beforeMount前需要執行模板編譯邏輯
callHook(vm, 'beforeMount')
let 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
// 觸發mounted事件
callHook(vm, 'mounted')
}
return vm
}
beforeMount
我們知道vue需要render函數來生成vnode。但是在實際開發中,基本都是通過template和el來指定模板,很少直接提供一個render函數。因此在觸發beforeMount前,vue最重要的一個工作就是將HTML模板編譯成render函數。beforeMount鉤子函數被調用時,我們尚不能訪問DOM元素。
mounted
每個vue實例都會對應一個render watcher。render watcher?會創建vnode(通過_render方法),并對vnode進行diff后,創建或者更新DOM元素(通過_update方法)。對于初次渲染來說,當創建完DOM元素后,把DOM樹的根元素插入到body中,然后觸發mounted鉤子函數。此時,在鉤子函數中可以對DOM元素進行操作了。
更新
實例完成初始化和掛載之后,如果由于用戶的交互導致實例的狀態發生了變化,實例將進入更新階段。例如在代碼中執行?this.msg = 'update msg',vue實例需要更新DOM元素。
實例的更新是異步的。前面提到過,render watcher?會負責調度程序創建vnode、創建更新DOM元素。當數據發生變化后,vue不會立即啟動DOM的更新,而是先把實例對應的render watcher添加到一個隊列中。然后在下一個事件循環中,統一執行DOM更新,清空隊列。也就是調用下面代碼中的flushSchedulerQueue函數。
此階段會觸發的鉤子是:beforeUpdate?和updated。
/*** 清空所有的隊列并執行watcher的更新邏輯
*/
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 隊列按照watcher的id升序排序,目的是確保:
// 1. 組件總是從父向子進行更新
// 2. 用戶創建的watcher先于渲染watcher更新
// 3. 如果組件在父組件的watcher運行時被銷毀,該組件的watcher可以跳過處理
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 調用watcher.before,觸發beforeUpdate鉤子
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// 更新dom
watcher.run()
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// 觸發activated鉤子
callActivatedHooks(activatedQueue)
// 觸發updated鉤子
callUpdatedHooks(updatedQueue)
}
// 觸發updated鉤子
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')
}
}
}
beforeUpdate
在初始化階段,vue實例中的數據(data/props/computed/watch)已經被處理成響應式的了。任何對數據的訪問(getter)都會被watcher添加為依賴,任何對數據的變更,都將觸發對數據有依賴的watcher的更新。watcher在更新之前會調用watcher.before方法,該方法是在掛載階段創建watcher實例的時候定義的。watcher.before中會觸發beforeUpdate鉤子。此時vue實例只是確定了最終需要更新的數據,尚未真正開始更新。
updated
從代碼中可以看到,在觸發updated鉤子前,vue實例需要對DOM元素進行更新。更新的過程是異步的。具體方式通過實例的render watcher執行run方法。該方法會去調用我們在掛載階段介紹的updateComponent函數。從而重新創建vnode,并進行vnode的diff操作后更新DOM元素。
我們還注意到,代碼中調用了callActivatedHooks函數,該函數用來觸發activated鉤子。下文我們再做說明,這里不展開。
銷毀
當vue實例的$destroy方法時,實例將進入銷毀階段。此時觸發的鉤子是:beforeDestory和destroyed。
// 銷毀Vue實例Vue.prototype.$destroy = function () {
const vm: Component = this
// 避免重復執行銷毀操作
if (vm._isBeingDestroyed) {
return
}
// 觸發實例的beforeDestroy鉤子
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// 將實例從其父實例中的$chilren中移除(斷開與父實例的聯系)
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()
}
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
vm._isDestroyed = true
// 銷毀指令、ref等
vm.__patch__(vm._vnode, null)
// 觸發destroyed事件
callHook(vm, 'destroyed')
// 移除實例的所有事件監聽器
vm.$off()
if (vm.$el) {
vm.$el.__vue__ = null
}
// 釋放循環引用(#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
beforeDestroy
從代碼中可以看到,$destroy被調用時,在執行實際的銷毀動作前觸發beforeDestroy。此時,由于并未開始執行實際的銷毀代碼,實例及DOM元素仍可正常訪問。
destroyed
從代碼中可以看到,銷毀操作主要包括以下幾點:清空所有的watcher、刪除所有的指令、刪除DOM元素并關閉DOM上的所有的事件、斷開與父實例之間的關系、將實例標記成已銷毀狀態。此時實例已經被銷毀,已經無法訪問實例的屬性和DOM元素了。
keep-alive包裹下的組件的生命周期鉤子
下面的兩個鉤子只有當組件包裹在keep-alive時才會觸發。
activated
HTML標簽和組件標簽在vue內部實現中都有對應的vnode,組件vnode在設計上與普通的HTML標簽的vnode有所不同。例如組件vnode上包含init、prepatch、insert、destroy等鉤子。這些鉤子在組件實例初始化、更新和銷毀等不同的階段進行調用。
// 組件vnode的鉤子const componentVNodeHooks = {
// ...省略其他鉤子
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
// 觸發組件的mounted鉤子
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
}
}
// 觸發activated鉤子
export function activateChildComponent (vm: Component, direct?: boolean) {
// ...省略部分代碼
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
// 調用實例的activated鉤子
callHook(vm, 'activated')
}
}
與根實例一樣,keep-alive包裹下的組件實例初始化時同樣會依次經歷初始化階段、掛載階段,但在掛載階段之后會調用組件vnode的insert鉤子,insert鉤子會觸發組件實例的activated鉤子。因為insert?是在組件實例掛載完成后調用的,所以mounted的觸發早于activated。
當組件切換回來的同時,組件的數據發生了變化,此時組件將進入更新階段,意味著將會依次觸發beforeUpdate和updated鉤子。
那么問題來了,activated、beforeUpdate和updated鉤子哪個先觸發呢?
答案是先觸發beforeUpdate,再觸發activated,最后觸發updated。
我們回顧下前面更新階段的代碼:
// 觸發activated鉤子callActivatedHooks(activatedQueue)
// 觸發updated鉤子
callUpdatedHooks(updatedQueue)
//...省略部分代碼
// 觸發activated鉤子
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
可以看到在函數flushSchedulerQueue中,callActivatedHooks函數用來觸發activated。而callActivatedHooks的順序在callUpdatedHooks前面,所以activated鉤子的觸發早于updated鉤子。
deactivated
const componentVNodeHooks = {// ... 省略其他鉤子
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
// 未被keep-alive包裹銷毀組件
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
// 被keep-alive包裹
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
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])
}
// 調用實例的deactivated鉤子
callHook(vm, 'deactivated')
}
}
當組件被切換到其他的組件時,會調用組件vnode的destroy鉤子,在組件被keep-alive包裹的情況下,只會將組件對應的DOM元素從DOM樹刪除,但不會銷毀組件實例,此時會調用deactivateChildComponent?從而觸發deactivated鉤子。組件在沒有被keep-alvie包裹的情況下,才會調用$destroy銷毀組件實例,觸發beforeDestroy和destroyed鉤子。
總結
關于vue實例的生命周期,官網講解的其實是比較簡單易懂的。本文主要還是希望能從源碼的角度,進一步讓大家理解每個生命周期做了什么處理。更好地理解鉤子的觸發時機及先后順序。歡迎大家留言討論~
推薦閱讀Vue執行流程解析
vue編譯過程分析
vue渲染過程解析-VDOM &DOM
vue組件實現原理解析
keep-alive是如何保持組件狀態的
好文我在看?
總結
以上是生活随笔為你收集整理的vue 生命周期_深入理解Vue实例生命周期的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vs最好的版本_Win10 环境下,Li
- 下一篇: vue each_Vue.js从零开始—