高級前端開發者必會的34道Vue面試題解析(四)

​讀完本文你將知道
javascript

一、Vue的生命週期是什麼?
vue

二、Vue中的鉤子函數java

三、Ajax請求放在哪一個鉤子函數中?node

四、beforeDestroy什麼時候使用?git

注意:本文的vue版本爲:2.6.11。github

Vue的生命週期是什麼?

每一個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中的鉤子函數

接下來咱們從兩個層面看看Vue中的鉤子函數執行。第一,從開發者的代碼層面看看,與開發者較爲密切的數據模型與頁面DOM結構在各個生命週期鉤子函數執行時的變化。第二,在源碼層面看一下這些生命週期鉤子函數它們各自的執行過程。微信

下面是源碼裏所列出來的全部可承載開發者代碼的鉤子函數。

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];複製代碼

beforeCreate與created

能夠看到beforeCreate在執行的時候,data尚未被初始化,DOM也沒有初始化,因此不能在這裏發起異步請求而且不能給數據模型的屬性賦值。

與beforeCreate不一樣的是,created被執行的時候數據模型下的val已經完成了初始化工做,可是頁面DOM依舊不能獲取到。說明在created裏,咱們能夠發起異步請求進行數據模型的賦值操做,可是不能作頁面DOM的操做。

beforeCreate與created執行源碼解析

// 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與Mounted

能夠從下面的源碼裏看到,beforeMount與created之間只有一個是不是瀏覽器的判斷,因此這時候在鉤子函數中的裏數據模型裏、頁面的狀態,與created是同樣的。

mounted被執行到的時候,數據模型和頁面的DOM都初始化完成,在這裏咱們能夠給數據模型賦值也能夠進行DOM操做了。

beforeMount與Mounted源碼解析

// _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與Update

這裏要注意下,beforeUpdate裏的代碼並不像前面四個鉤子函數會把自動執行,而是經過操做數據模型裏的值來觸發執行的,圖上的例子中,因爲mounted的this.val='56789'執行,形成了beforeUpdate的執行,並且在beforeUpdate執行的時候,數據模型裏的值已是操做後的最新值。

Update的執行在beforeUpdate以後,與beforeUpdate的數據與頁面保持一致。

beforeUpdate與Update源碼解析

...  
// 啓動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內的代碼
    }
  }
}複製代碼

activated與deactivated

在 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)
  }
})複製代碼

activated觸發源碼

它只有被標籤緩存的組件激活的時候纔會被調用。

// 當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的執行

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');
  }
}複製代碼

beforeDestroy與destoryed

在mounted手動進行了destory銷燬組件,觸發了beforeDestroy鉤子函數執行,在這裏依舊能看到數據模型與DOM是未被註銷的。

在這裏咱們能夠看到DOM已經被清除了。

beforeDestroy與destoryed源碼解析

// 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;
  }
};複製代碼

errorCaptured

2.5.0+以後引入的鉤子函數,目的是爲了穩定性,當子孫組件發生異常的時候,則會觸發這個鉤子函數,它有三個參數,錯誤對象、發生錯誤的組件實例、錯誤來源信息,能夠主動返回 false 阻止該錯誤繼續向上面的父組件傳播。

能夠看下面這個例子,我在子組件my-comp的mounted裏直接throw new Error,在外層組件裏的erroeCaptured鉤子函數獲得觸發執行。

errorCaptured源碼解析

能夠看出它的本質實際上是一個包裹子組件的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');
      }
    }
  }
}複製代碼

serverPrefetch

這個方法是2.6+裏新增的且只能在服務端渲染時能獲得觸發的鉤子函數,它會返回一個promise,由於這裏無法用瀏覽器調試,暫時不介紹這個API,待後續再細寫。

Ajax請求放在哪一個鉤子函數中?

仔細看完了上面解析,咱們即可清楚的知道,Ajax請求應該放在created鉤子函數是最好的,這時候數據模型data已經初始化好了。

若是放在beforeCreate函數裏,這時候data尚未初始化,沒法將獲取到的數據賦值給數據模型。

若是放在mounted裏,這時候頁面結構已經完成,若是獲取的數據與頁面結構無聯繫的話,這個階段是略微有點遲的。

beforeDestroy什麼時候使用?

實際對於銷燬的場景大部分使用的destroy就足夠了,而beforeDestroy什麼時候使用呢?

看看它倆的區別,beforeDestroy執行的時候頁面DOM仍是存在未被銷燬的,而Destroy執行的時候,頁面已經從新渲染完了,因此咱們能夠在beforeDestroy裏執行一些組件銷燬前對頁面的特殊操做。


References

[1] https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.common.dev.js

[2] https://cn.vuejs.org/

後記

若是你喜歡探討技術,或者對本文有任何的意見或建議,你能夠掃描下方二維碼,關注微信公衆號「 全棧者 」,也歡迎加做者微信,與做者隨時互動。歡迎!衷心但願能夠碰見你。

有問題的小夥伴 歡迎加羣交流~

相關文章
相關標籤/搜索