從源碼瞭解Vue生命週期

從源碼瞭解Vue生命週期

渣新一枚,近期打算離職,去好點的城市發展,而後發現寫了很多業務代碼,真正有用的又一問三不知。 雖然一直都在用Vue,但總以爲好像只是背了背文檔,成了一個Api調用師。基於職業發展焦慮, 打算好好學習學習,把看到的東西總結下,即使快過期了,面試也可能會用到。既方便之後複習,也分享給你們,有錯還請指正,寫的很差望海涵。。vue

生命週期概覽[2.x]

  1. beforeCreate // 調用該生命週期前已初始化生命週期,事件和渲染函數,不能訪問到props等屬性
  2. created // 調用該生命週期前已順序初始化具體的數據—— injections => props => methods => data => computed => watch => initProvide
  3. beforeMount // 調用該生命週期前已初始化渲染函數$options.render
  4. mounted // 調用該生命週期前已渲染真實節點
  5. beforeUpdate // 狀態改變時,會在nextTick中更新視圖前調用
  6. updated // 已調用render函數從新渲染
  7. activated // keep-alive緩存組件渲染時調用(mounted前會渲染一次,每次updated前也會渲染一次)
  8. deactivated // keep-alive緩存組件銷燬後調用
  9. beforeDestroy
  10. destroyed
  11. errorCaptured

咱們再來看看源碼中是如何一步一步建立一個Vue實例的node

new Vue(options)

new Vue(options)顯示建立一個Vue實例,傳入實例配置屬性options,咱們就來看看Vue構造函數作了什麼?react

function Vue ( options ) {
 if ( !( this instanceof Vue ) ) {  warn( 'Vue is a constructor and should be called with the `new` keyword' );  }  this._init( options );  } 複製代碼

構造函數就只作了一件事,調用內部方法_init() , _init是混入Vue原型對象上的一個方法,咱們繼續往下看。web

Vue.prototype._init

_init簡要代碼以下(已省略於本文無關代碼)面試

Vue.prototype._init = function ( options ) {
 var vm = this;  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 ); // init props methods data computed watch  initProvide( vm ); // resolve provide after data/props  callHook( vm, 'created' );   if ( vm.$options.el ) {  vm.$mount( vm.$options.el );  } }  複製代碼

_init函數中,mergeOptions將傳入的配置和諸如Mixin配置按照默認或用戶自定義的合併策略進行合併。 配置項合併完成後,就能夠開始初始化組件數據了。緩存

initLifecycle

function initLifecycle ( vm ) {
 var options = vm.$options;   // locate first non-abstract parent  var parent = options.parent;  if ( parent && !options.abstract ) {  while ( parent.$options.abstract && parent.$parent ) {  parent = parent.$parent;  }  parent.$children.push( vm );  }   vm.$parent = parent;  vm.$root = parent ? parent.$root : vm;   vm.$children = [];  vm.$refs = {};   vm._watcher = null;  vm._inactive = null;  vm._directInactive = false;  vm._isMounted = false;  vm._isDestroyed = false;  vm._isBeingDestroyed = false;  } 複製代碼

initLifecycle中首先循環查找當前組件的父級(非抽象),並掛在到$parent屬性上,而後初始化了其餘的一些屬性的默認值。 說明beforeCreate此時已經能夠訪問$parent$root編輯器

initEvents

function initEvents ( vm ) {
 vm._events = Object.create( null );  vm._hasHookEvent = false;  // init parent attached events  var listeners = vm.$options._parentListeners;  if ( listeners ) {  updateComponentListeners( vm, listeners );  } } 複製代碼

initEvents也不復雜,初始化內部屬性並獲取父組件註冊到該組件上的事件並初始化(updateComponentListeners)。 其中_parentListeners是父組件傳入的事件監聽,_parentListeners存在時將會在子組件內進行事件註冊。ide

這裏須要注意的是,initEvents初始化的是父組件傳入的事件監聽。函數

initRender

function initRender ( vm ) {
 var options = vm.$options;  var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree  var renderContext = parentVnode && parentVnode.context;  vm.$slots = resolveSlots( options._renderChildren, renderContext );  vm.$scopedSlots = emptyObject;   vm._c = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, false ); };  vm.$createElement = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, true ); };   // $attrs & $listeners are exposed for easier HOC creation.  // they need to be reactive so that HOCs using them are always updated  var parentData = parentVnode && parentVnode.data;  {  defineReactive$$1( vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {  !isUpdatingChildComponent && warn( "$attrs is readonly.", vm );  }, true );  defineReactive$$1( vm, '$listeners', options._parentListeners || emptyObject, function () {  !isUpdatingChildComponent && warn( "$listeners is readonly.", vm );  }, true );  }  } 複製代碼

這裏能夠看到,initRender初始化一些屬性,其中主要的函數$createElement已經能在實例中訪問。代碼的最後幾行中, 還將父組件的attrslisteners(都作了響應式處理)掛載到了子組件實例中,主要是爲了高階組件的使用。oop

callHook( vm, 'beforeCreate' )

在調用beforeCreate前,vue實例已經能訪問$parent, $root, $listeners, $attrs, $createElement, $slots屬性了。 這些屬性大部分都是從父組件中傳入的。

initInjections(vm)

function initInjections ( vm ) {
 var result = resolveInject( vm.$options.inject, vm );  if ( result ) {  toggleObserving( false );  Object.keys( result ).forEach( function ( key ) {  /* istanbul ignore else */  {  defineReactive$1( vm, key, result[key], function () {  warn(  "Avoid mutating an injected value directly since the changes will be " +  "overwritten whenever the provided component re-renders. " +  "injection being mutated: \"" + key + "\"",  vm  );  } );  }  } );  toggleObserving( true );  } } 複製代碼

initInjections僅僅對inject選項作處理,resolveInject函數內部會層層遍歷父節點,查找全部注入的屬性並將inject相關屬性轉換成鍵值對的形式。 拿到鍵值對後,再逐一將這些注入屬性掛載到當前實例下。

須要注意的是,掛載前執行了toggleObserving函數,傳入false時,後續綁定的屬性將不會主動設置爲響應式,也就是說,inject屬性一般都並不是響應的(除非它自己就是響應式)。 inject初始化後再恢復響應式綁定=>toggleObserving( true )

initState

function initState ( vm ) {
 vm._watchers = [];  var opts = vm.$options;  if ( opts.props ) { initProps( vm, opts.props ); }  if ( opts.methods ) { initMethods( vm, opts.methods ); }  if ( opts.data ) {  initData( vm );  } else {  observe( vm._data = {}, true /* asRootData */ );  }  if ( opts.computed ) { initComputed( vm, opts.computed ); }  if ( opts.watch && opts.watch !== nativeWatch ) {  initWatch( vm, opts.watch );  }  } 複製代碼

從代碼中就能夠看出來initState都作了什麼,看到這裏,咱們能夠記住created前已經初始化了props methods data computed watch屬性。

initProvide

function initProvide ( vm ) {
 var provide = vm.$options.provide;  if ( provide ) {  vm._provided = typeof provide === 'function'  ? provide.call( vm )  : provide;  }  } 複製代碼

initProvide只作了一件事,將組件提供的Provide選項保存到私有屬性_provided中。

callHook( vm, 'created' )

至此,咱們已經能訪問絕大部分的組件屬性了,如data, props, methods, inject

vm.$mount

_init函數最後一步就是掛在實例到DOM中,用到的就是$mount函數。

$mount涉及到的代碼可能有些多,篇幅問題,咱們就看看主要步驟。

Vue.prototype.$mount = function (el, hydrating) {
if ( !options.render ) {  var template = options.template;  if ( template ) {  // 對template模板進行校驗處理  } else if ( el ) {  template = getOuterHTML( el );  }  if ( template ) {  var ref = compileToFunctions( template, {  outputSourceRange: "development" !== 'production',  shouldDecodeNewlines: shouldDecodeNewlines,  shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,  delimiters: options.delimiters,  comments: options.comments  }, this );  var render = ref.render;  var staticRenderFns = ref.staticRenderFns;  options.render = render;  options.staticRenderFns = staticRenderFns;  }  }  return mount.call( this, el, hydrating )  }; 複製代碼

$mount根據手寫render函數template選項和el選項決定如何渲染掛在實例。 其中比較主要的就是compileToFunctions函數。 咱們看到該函數傳入了template並返回了render,staticRenderFns等函數。 很容易猜到Vue在這裏對模板進行了解析。返回的render也就是咱們常說的渲染函數。

模板轉換爲渲染函數有三個步驟:

  1. 模板解析 template => AST
  2. 優化 => 標記靜態節點
  3. 轉化爲渲染函數

有渲染函數後,咱們就能掛在到真實節點上了mount.call( this, el, hydrating )

mount

mount function ( el, hydrating ) {
 el = el && inBrowser ? query( el ) : undefined;  return mountComponent( this, el, hydrating ) };  function mountComponent ( vm, el, hydrating ) {  vm.$el = el;  if ( !vm.$options.render ) {  vm.$options.render = createEmptyVNode;  }  callHook( vm, 'beforeMount' );  ...  } 複製代碼

mountComponent 函數中,確保了render函數存在。緊隨其後調用了beforeMount生命週期。 咱們回顧如下createdbeforeMount,它們之間進行了模板編譯,優化,轉換爲渲染函數。

// 緊接剛纔的代碼
 var updateComponent;  updateComponent = function () {  vm._update( vm._render(), hydrating );  };  new Watcher( vm, updateComponent, noop, {  before: function before () {  if ( vm._isMounted && !vm._isDestroyed ) {  callHook( vm, 'beforeUpdate' );  }  }  }, true /* isRenderWatcher */ );  hydrating = false;   if ( vm.$vnode == null ) {  vm._isMounted = true;  callHook( vm, 'mounted' );  }  return vm 複製代碼

這裏主要是初始化了Watcher實例,實例中傳入了updateComponent函數。 該函數後續會對比虛擬Dom並更新視圖。初始化了Watcher時就會執行一次updateComponent。 而後callHook( vm, 'mounted' )。 因此mounted階段已經能訪問真實DOM了。

beforeUpdate updated

new Watcher( vm, updateComponent, noop, {
 before: function before () {  if ( vm._isMounted && !vm._isDestroyed ) {  callHook( vm, 'beforeUpdate' );  }  }  }, true /* isRenderWatcher */ ); 複製代碼

new Watche 的before中調用了beforeUpdate,順着before,我找到了flushSchedulerQueue函數。

function flushSchedulerQueue () {
 currentFlushTimestamp = getNow();  flushing = true;  var watcher, id;  ...  // do not cache length because more watchers might be pushed  // as we run existing watchers  for ( index = 0; index < queue.length; index++ ) {  watcher = queue[index];  if ( watcher.before ) {  watcher.before();  }  ...  watcher.run();  ...  }  ...  // call component updated and activated hooks  callActivatedHooks( activatedQueue );  callUpdatedHooks( updatedQueue );  ...  } 複製代碼

當須要更新依賴/狀態時($forceUpdate, 數據讀取變更),flushSchedulerQueue就會被觸發。

flushSchedulerQueue先遍歷了watcher,調用了before(也就是生命週期beforeUpdate), 而後執行callActivatedHook函數,該函數調用其子組件activated鉤子, 最後再調用callUpdatedHooks也就是updated鉤子。

activated

來看看activated, keep-alive緩存的組件激活時調用

componentVNodeHooks: {
 insert: function insert ( vnode ) {  var context = vnode.context;  var componentInstance = vnode.componentInstance;  if ( !componentInstance._isMounted ) {  componentInstance._isMounted = true;  callHook( componentInstance, 'mounted' );  }  if ( vnode.data.keepAlive ) {  if ( context._isMounted ) {  queueActivatedComponent( componentInstance );  } else {  activateChildComponent( componentInstance, true /* direct */ );  }  }  }, ... } 複製代碼

以上代碼能夠看到,mounted調用後會判斷是否爲緩存組件並調用activated。

beforeDestroy destoryed deactivated

調用$destory時觸發beforeDestroydeactivated,完成數據銷燬後調用destoryed

好記性不如爛筆頭,看了很快就忘了,仍是記一記能有個好的思路,也方便複習。

本文使用 mdnice 排版

相關文章
相關標籤/搜索