Vue.js原始碼 生命週期 LifeCycle 學習
callHook
。
我們來看看 callHook 程式碼:
export function callHook (vm: Component, hook: string) { const handlers = vm.$options[hook] // 獲取Vue選項中的生命週期鉤子函式 if (handlers) { 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) } }
// 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) // 初始化資料
initProvide(vm) // 初始化Provide
callHook(vm, 'created')
……
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 如果有el屬性,將內容掛載到el中去。
}
}
beforeMount & mounted
beforeMount
在掛載開始之前被呼叫:相關的 render 函式首次被呼叫。該鉤子在伺服器端渲染期間不被呼叫。
mounted
el 被新建立的 vm.el替換,並掛載到例項上去之後呼叫該鉤子。如果root例項掛載了一個文件內元素,當mounted被呼叫時vm.el替換,並掛載到例項上去之後呼叫該鉤子。如果root例項掛載了一個文件內元素,當mounted被呼叫時vm.el 也在文件內。
貼出程式碼邏輯// 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 }
那麼這個 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初始化的時候,判斷是否有 el,如果有則執行 $mount 方法。
// src/core/instance/init.js
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 如果有el屬性,將內容掛載到el中去。
}
至此生命週期邏輯應該是 beforeCreate - created - beforeMount -mounted
beforeUpdate & updated
beforeUpdate
資料更新時呼叫,發生在虛擬 DOM 打補丁之前。這裡適合在更新之前訪問現有的 DOM,比如手動移除已新增的事件監聽器。
updated
由於資料更改導致的虛擬 DOM 重新渲染和打補丁,在這之後會呼叫該鉤子。
找程式碼邏輯~ beforeUpdate 和 updated 在兩個地方呼叫。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 如果是已經掛載的,就觸發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.
}
在執行 _update 方法的時候,如果 DOM 已經掛載了,則呼叫 beforeUpdate 方法。
在 _update 方法的最後作者也注視了呼叫 updated hook 的位置:updated 鉤子由 scheduler 呼叫來確保子元件在一個父元件的 update 鉤子中。
我們找到 scheduler,發現有個 callUpdateHooks 方法,該方法遍歷了 watcher 陣列。
// 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')
}
}
}
這個 callUpdatedHooks 在 flushSchedulerQueue 方法中呼叫。
/**
* 重新整理佇列並執行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()
// 呼叫元件的updated和activated生命週期
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
}
繼續找下去
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true // 此引數用於判斷watcher的ID是否存在
……
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
最終在 watcher.js 找到 update 方法:
// src/core/observer/watcher.js
update () {
// lazy 懶載入
// sync 元件資料雙向改變
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // 排隊watcher
}
}
等於是佇列執行完 Watcher 陣列的 update 方法後呼叫了 updated 鉤子函式。
beforeDestroy & destroyed
beforeDestroy
例項銷燬之前呼叫。在這一步,例項仍然完全可用。該鉤子在伺服器端渲染期間不被呼叫。
destroyed
Vue 例項銷燬後呼叫。呼叫後,Vue 例項指示的所有東西都會解繫結,所有的事件監聽器會被移除,所有的子例項也會被銷燬。該鉤子在伺服器端渲染期間不被呼叫。
看程式碼~
// src/core/instance/lifecycle.js
// 銷燬方法
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
// 已經被銷燬
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)
// 觸發 destroyed 鉤子
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
}
這是一個銷燬 Vue 例項的過程,將各種配置清空和移除。
activated & deactivated
activated
keep-alive 元件啟用時呼叫。
deactivated
keep-alive 元件停用時呼叫。
找到實現程式碼的地方
// 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')
}
}
以上兩個方法關鍵就是修改了 vm._inactive 的值,並且鄉下遍歷子元件,最後觸發鉤子方法。
errorCaptured
當捕獲一個來自子孫元件的錯誤時被呼叫。此鉤子會收到三個引數:錯誤物件、發生錯誤的元件例項以及一個包含錯誤來源資訊的字串。此鉤子可以返回 false 以阻止該錯誤繼續向上傳播。
這是 2.5 以上版本有的一個鉤子,用於處理錯誤。
// src/core/util/error.js
export function handleError (err: Error, vm: any, info: string) {
if (vm) {
let cur = vm
// 向上冒泡遍歷
while ((cur = cur.$parent)) {
// 獲取鉤子函式
const hooks = cur.$options.errorCaptured
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
try {
// 執行 errorCaptured 鉤子函式
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還提供了生命週期方法來直接呼叫。
vm.$mount
如果 Vue 例項在例項化時沒有收到 el 選項,則它處於“未掛載”狀態,沒有關聯的 DOM 元素。可以使用 vm.$mount() 手動地掛載一個未掛載的例項。
如果沒有提供 elementOrSelector 引數,模板將被渲染為文件之外的的元素,並且你必須使用原生 DOM API 把它插入文件中。
這個方法返回例項自身,因而可以鏈式呼叫其它例項方法。
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
}
}
// 執行 $mount 方法
return mount.call(this, el, hydrating)
}
其實很簡單,先獲取html程式碼,然後執行 compileToFunctions 方法執行編譯過程(具體編譯過程在學習Render的時候再說)。
vm.$forceUpdate
迫使 Vue 例項重新渲染。注意它僅僅影響例項本身和插入插槽內容的子元件,而不是所有子元件。
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
這是強制更新方法,執行了 vm._watcher.update() 方法。
vm.$nextTick
將回調延遲到下次 DOM 更新迴圈之後執行。在修改資料之後立即使用它,然後等待 DOM 更新。它跟全域性方法 Vue.nextTick 一樣,不同的是回撥的 this 自動繫結到呼叫它的例項上。
找了找 vm.$nextTick 的程式碼
// src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
找到這個 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
})
}
}
具體功能邏輯等學習完 render 再更新……
vm.$destroy
完全銷燬一個例項。清理它與其它例項的連線,解綁它的全部指令及事件監聽器。
觸發 beforeDestroy 和 destroyed 的鉤子。
關於$destroy 我們之前再說 destroyed 鉤子的時候提到過了,這裡就不再贅述。
Vue.prototype.$destroy = function () {
……
}