讀完本文你將知道
javascript
一、Vue的生命週期是什麼?
vue
二、Vue中的鉤子函數java
三、Ajax請求放在哪一個鉤子函數中?node
四、beforeDestroy什麼時候使用?git
注意:本文的vue版本爲:2.6.11。github
每一個new出來的Vue實例都會有從實例化建立、初始化數據、編譯模板、掛載DOM、數據更新、頁面渲染、卸載銷燬等一系列完整的、從「生」到「死」的過程,這個過程即被稱之爲生命週期。promise
在生命週期的每一個節點,Vue提供了一些鉤子函數,使得開發者的代碼能被有機會執行。這裏的鉤子函數能夠簡單理解爲,在Vue實例中預先定義了一些像created,mounted等特定名稱的函數,函數體的內容開發給開發者填充,當被實例化的時候,會按照肯定的前後順序來執行這些鉤子函數,從而將開發者的代碼有機會執行。瀏覽器
對於如何在Vue內部調用開發者的代碼原理,能夠看看下面這個例子。緩存
// 好比這是Vue的源碼
function Vue(options) {
console.log('初始化');
// 開始執行一些代碼
console.log('開始建立');
options.created();
// 開始執行一些代碼
console.log('建立完成');
options.mounted();
console.log('其餘操做');
}
// 實例化Vue構造函數
new Vue({
// 掛載兩個方法
created () {
console.log('我是開發者的代碼, 我須要在建立完成前執行')
},
mounted () {
console.log('我是開發者的代碼, 我須要在建立完成後執行')
},
})
/** 初始化 開始建立 我是開發者的代碼, 我須要在建立完成前執行 建立完成 我是開發者的代碼, 我須要在建立完成後執行 其餘操做 */複製代碼
接下來咱們從兩個層面看看Vue中的鉤子函數執行。第一,從開發者的代碼層面看看,與開發者較爲密切的數據模型與頁面DOM結構在各個生命週期鉤子函數執行時的變化。第二,在源碼層面看一下這些生命週期鉤子函數它們各自的執行過程。微信
下面是源碼裏所列出來的全部可承載開發者代碼的鉤子函數。
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];複製代碼
能夠看到beforeCreate在執行的時候,data尚未被初始化,DOM也沒有初始化,因此不能在這裏發起異步請求而且不能給數據模型的屬性賦值。
與beforeCreate不一樣的是,created被執行的時候數據模型下的val已經完成了初始化工做,可是頁面DOM依舊不能獲取到。說明在created裏,咱們能夠發起異步請求進行數據模型的賦值操做,可是不能作頁面DOM的操做。
// Vue入口
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
// 調用_init方法
this._init(options);
}
// _init實現
Vue.prototype._init = function (options) {
var vm = this;
...
initLifecycle(vm); //初始化生命週期
initEvents(vm); //初始化事件監聽
initRender(vm); //初始定義渲染選項,而且對一些屬性進行監聽。
//執行開發者的beforeCreate內的代碼
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);
}
};
// Vue中調用鉤子函數的封裝函數
function callHook (vm, hook) {
...
// 開發者寫好的某hook函數
var handlers = vm.$options[hook];
...
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
...
// 封裝好的調用開發者方法
invokeWithErrorHandling(handlers[i], vm, null, vm, info);
...
}
}
...
}
// 執行hook函數
function invokeWithErrorHandling (handler,context,args,vm,info) {
var res;
try {
// 調用執行
res = args ? handler.apply(context, args) : handler.call(context);
...
} catch (e) {
handleError(e, vm, info);
}
}複製代碼
能夠從下面的源碼裏看到,beforeMount與created之間只有一個是不是瀏覽器的判斷,因此這時候在鉤子函數中的裏數據模型裏、頁面的狀態,與created是同樣的。
mounted被執行到的時候,數據模型和頁面的DOM都初始化完成,在這裏咱們能夠給數據模型賦值也能夠進行DOM操做了。
// _init實現
Vue.prototype._init = function (options) {
var vm = this;
...
if (vm.$options.el) {
// 掛載執行
vm.$mount(vm.$options.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; //this.$el開始掛載到實例中
...
callHook(vm, 'beforeMount'); // 執行開發者的beforeMount內的代碼
...
updateComponent = function () { // 定義全局更新函數updateComponent
vm._update(vm._render(), hydrating);
};
...
// 啓動Watcher,綁定vm._watcher屬性
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
// 執行開發者的beforeUpdate內的代碼
callHook(vm, 'beforeUpdate');
}
},
}, true /* isRenderWatcher */);
if (vm.$vnode == null) {
vm._isMounted = true;
// 執行開發者的mounted內的代碼
callHook(vm, 'mounted');
}
return vm
}
// Watch構造函數
var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
...
// 將上面的updateComponent進行復制給this.getter 屬性
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
...
}
}
...
// 調用get方法
this.get()
};
// watcher的get方法運行getter方法
Watcher.prototype.get = function get () {
...
var vm = this.vm;
try {
// 實際執行了Vue的構造函數裏的_init方法定義的updateComponent函數
// vm._update(vm._render(), hydrating);
value = this.getter.call(vm, vm);
} catch (e) {
...
return value
};
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
...
// 渲染頁面,更新節點
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
...
};複製代碼
這裏要注意下,beforeUpdate裏的代碼並不像前面四個鉤子函數會把自動執行,而是經過操做數據模型裏的值來觸發執行的,圖上的例子中,因爲mounted的this.val='56789'執行,形成了beforeUpdate的執行,並且在beforeUpdate執行的時候,數據模型裏的值已是操做後的最新值。
Update的執行在beforeUpdate以後,與beforeUpdate的數據與頁面保持一致。
...
// 啓動Watcher,綁定vm._watcher屬性
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate'); // 執行開發者的beforeUpdate內的代碼
}
},
}, true /* isRenderWatcher */);
...
//數據模型裏面的值變化時觸發該函數(能夠看上一篇文章)
// 例如this.val=345改變data裏的val屬性的時候,該函數將獲得執行。
function flushSchedulerQueue () {
...
var watcher, id
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
//觸發beforeUpdate的鉤子函數
watcher.before();
}
}
...
//調用activate的鉤子函數
callActivatedHooks(activatedQueue);
//調用update的鉤子函數
callUpdatedHooks(updatedQueue);
...
}
// 調用updated鉤子函數
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) { // 輪詢隊列裏全部的變化
var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated'); // 執行開發者的updated內的代碼
}
}
}複製代碼
在 2.2.0 及其更高版本中,activated鉤子函數和deactivated鉤子函數被引用進來,由於這兩個鉤子函數只會是被keep-alive標籤包裹的子組件纔會獲得觸發機會,因此不多被人注意到,先看一個入門例子。
import Vue from './node_modules/_vue@2.6.11@vue/dist/vue.common.dev'
new Vue({
el: '#app',
template: ` <div id="app"> <keep-alive> <my-comp v-if="show" :val="val"></my-comp> </keep-alive> </div>`,
data () { return { val: '12345', show: true } },
components: {
// 自定義子組件my-comp
'my-comp': {
template: '<div>{{val}}</div>',
props: [ 'val' ],
activated() {
debugger; // 加載時觸發執行
},
deactivated() {
debugger; //兩秒後觸發執行
}
}
},
mounted() {
setTimeout(() => {
this.show = false
}, 2000)
}
})複製代碼
它只有被標籤緩存的組件激活的時候纔會被調用。
// 當keep-alive的子組件被激活的時候insert方法將獲得執行
// 也就是上面例子中this.show = true的時候
insert: function insert (vnode) {
var context = vnode.context;
var componentInstance = vnode.componentInstance;
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
// 先調用keep-alive子組件的mounted鉤子方法
callHook(componentInstance, 'mounted');
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// 若是外部組件是已經加載完成的,即上面例子裏的show初始爲false,加載完後this.show=true
// 將callActivatedHooks所調用的activatedQueue隊列push進去值
queueActivatedComponent(componentInstance);
} else {
// 若是外部組件未加載完成的。
// 就像上面例子的寫法,show初始爲true,加載完後this.show=false
// 而後在activateChildComponent直接觸發activated鉤子函數
activateChildComponent(componentInstance, true /* direct */);
}
}
}
//數據模型裏面的值變化時觸發該函數(能夠看上一篇文章)
//例如this.val=345改變data裏的val屬性的時候,該函數將獲得執行。
//執行的時候觸發callActivatedHooks函數,會在這時候調用activate鉤子函數
function flushSchedulerQueue () {
...
var watcher, id
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
//觸發beforeUpdate的鉤子函數
watcher.before();
}
}
...
//調用activate的鉤子函數
callActivatedHooks(activatedQueue);
//調用update的鉤子函數
callUpdatedHooks(updatedQueue);
...
}
// 數據模型data數據變化時觸發執行
function callActivatedHooks (queue) {
for (var i = 0; i < queue.length; i++) {
...
// 調用activated的鉤子函數執行
activateChildComponent(queue[i], true /* true */);
}
}
// 只有緩存的組件觸發該鉤子函數
function activateChildComponent (vm, direct) {
...
if (vm._inactive || vm._inactive === null) {
vm._inactive = false;
for (var i = 0; i < vm.$children.length; i++) {
// 遞歸調用子組件觸發其鉤子函數
activateChildComponent(vm.$children[i]);
}
// 執行開發者的activated鉤子函數內的代碼
callHook(vm, 'activated');
}
}複製代碼
deactivated鉤子函數的觸發是keep-alive標籤緩存的組件停用時觸發,像下面例子中被keep-alive標籤包裹的my-comp組件,當子組件被v-if置爲false的時候,deactivated鉤子函數將獲得執行。
deactivated的觸發源碼
//對於deactivate的觸發,只會是子組件destroy方法執行時被調用,
function destroy (vnode) { // 調用組件註銷時觸發
if (!componentInstance._isDestroyed) {
// 當觸發的組件不是keep-alive標籤的組件時觸發$destroy
if (!vnode.data.keepAlive) {
// 觸發實例組件的註銷
componentInstance.$destroy();
} else {
// 觸發deactivated的鉤子函數
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
function deactivateChildComponent (vm, direct) {
...
if (!vm._inactive) {
vm._inactive = true;
for (var i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i]); //遞歸執行觸發deactivated鉤子函數
}
// 執行開發者的deactivated內的代碼
callHook(vm, 'deactivated');
}
}複製代碼
在mounted手動進行了destory銷燬組件,觸發了beforeDestroy鉤子函數執行,在這裏依舊能看到數據模型與DOM是未被註銷的。
在這裏咱們能夠看到DOM已經被清除了。
// Vue的原型鏈方法 $destroy
Vue.prototype.$destroy = function () {
var vm = this;
...
// 執行開發者的beforeDestroy內的代碼
callHook(vm, 'beforeDestroy');
...
var parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm);
}
// 將數據監聽移除
if (vm._watcher) {
vm._watcher.teardown();
}
var i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--;
}
// 調用一次渲染,將頁面dom樹置爲null
vm.__patch__(vm._vnode, null);
//調用開發者的destroyed鉤子函數代碼
callHook(vm, 'destroyed');
// 關閉時間監聽
vm.$off();
// 移除Vue的全部依賴
if (vm.$el) {
vm.$el.__vue__ = null;
}
// 節點置爲null
if (vm.$vnode) {
vm.$vnode.parent = null;
}
};複製代碼
2.5.0+以後引入的鉤子函數,目的是爲了穩定性,當子孫組件發生異常的時候,則會觸發這個鉤子函數,它有三個參數,錯誤對象、發生錯誤的組件實例、錯誤來源信息,能夠主動返回 false 阻止該錯誤繼續向上面的父組件傳播。
能夠看下面這個例子,我在子組件my-comp的mounted裏直接throw new Error,在外層組件裏的erroeCaptured鉤子函數獲得觸發執行。
能夠看出它的本質實際上是一個包裹子組件的try catch,將全部捕獲到的異常內容作了一次攔截,而且在catch的時候決定是否繼續往外層拋錯。
// errorCaptured的執行則不經過callHook來執行,而是直接取了$options.errorCaptured來執行
function handleError (err, vm, info) {
...
var hooks = cur.$options.errorCaptured;
if (hooks) {
for (var i = 0; i < hooks.length; i++) {
try {
// 執行開發者定義的errorCaptured函數
var capture = hooks[i].call(cur, err, vm, info) === false;
// 若是鉤子函數返回爲false時,直接return,不在往上傳播錯誤
if (capture) { return }
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook');
}
}
}
}複製代碼
這個方法是2.6+裏新增的且只能在服務端渲染時能獲得觸發的鉤子函數,它會返回一個promise,由於這裏無法用瀏覽器調試,暫時不介紹這個API,待後續再細寫。
仔細看完了上面解析,咱們即可清楚的知道,Ajax請求應該放在created鉤子函數是最好的,這時候數據模型data已經初始化好了。
若是放在beforeCreate函數裏,這時候data尚未初始化,沒法將獲取到的數據賦值給數據模型。
若是放在mounted裏,這時候頁面結構已經完成,若是獲取的數據與頁面結構無聯繫的話,這個階段是略微有點遲的。
實際對於銷燬的場景大部分使用的destroy就足夠了,而beforeDestroy什麼時候使用呢?
看看它倆的區別,beforeDestroy執行的時候頁面DOM仍是存在未被銷燬的,而Destroy執行的時候,頁面已經從新渲染完了,因此咱們能夠在beforeDestroy裏執行一些組件銷燬前對頁面的特殊操做。
[1] https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.common.dev.js
[2] https://cn.vuejs.org/
若是你喜歡探討技術,或者對本文有任何的意見或建議,你能夠掃描下方二維碼,關注微信公衆號「 全棧者 」,也歡迎加做者微信,與做者隨時互動。歡迎!衷心但願能夠碰見你。
有問題的小夥伴 歡迎加羣交流~