詳解 Vue 生命週期實現

熱情的劉大爺發表於2019-02-21

前言

在我們平時使用各種框架的時候,都避免不了使用到一種特性,就是 生命週期 鉤子,這些鉤子,可以給我們提供很多便利,讓我們在資料更新的每一個階段,都可以捕捉到它的變化。

我們最主要講的是 vue 的生命週期,先來一份大綱:

  • beforeCreate(初始化介面前)
  • created(初始化介面後)
  • beforeMount(渲染dom前)
  • mounted(渲染dom後)
  • beforeUpdate(更新資料前)
  • updated(更新資料後)
  • beforeDestroy(解除安裝元件前)
  • destroyed(解除安裝元件後)

今天,我就來分析一下,vue 在呼叫到每一個生命週期前,到底都在做了什麼?

正文

來看看官方的生命週期流程圖:

詳解 Vue 生命週期實現

這張圖其實已經大概的告訴了我們,每個階段做了什麼,但是我覺得還有必要詳細的去分析一下,這樣在未來如果我們要實現類似於 vue 這種框架的時候,可以知道在什麼時間,應該去做什麼,怎麼去實現。

beforeCreate(初始化介面前)

function initInternalComponent (vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  // doing this because it's faster than dynamic enumeration.
  var parentVnode = options._parentVnode;
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;
  opts._parentElm = options._parentElm;
  opts._refElm = options._refElm;

  var vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}
function resolveConstructorOptions (Ctor) {
  var options = Ctor.options;
  if (Ctor.super) {
    var superOptions = resolveConstructorOptions(Ctor.super);
    var cachedSuperOptions = Ctor.superOptions;
    if (superOptions !== cachedSuperOptions) {
      // super 選項已更改,需要解決新選項。
      Ctor.superOptions = superOptions;
      // 檢查是否有任何後期修改/附加選項
      var modifiedOptions = resolveModifiedOptions(Ctor);
      // 更新基本擴充套件選項
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions);
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
      if (options.name) {
        options.components[options.name] = Ctor;
      }
    }
  }
  return options
}
if (options && options._isComponent) {
  initInternalComponent(vm, options);
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  );
}

if (process.env.NODE_ENV !== 'production') {
  initProxy(vm);
} else {
  vm._renderProxy = vm;
}

vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
複製程式碼

在一開始,先做了一個屬性的合併處理,如果 options 存在並且 _isComponenttrue ,那麼就呼叫 initInternalComponent 方法,這個方法最主要是優化內部元件例項化,因為動態選項合併非常緩慢,並且沒有內部元件選項需要特殊處理;

如果不滿足上述條件,就呼叫 mergeOptions 方法去做屬性合併,最後的返回值賦值給 $optionsmergeOptions 的實現原理,在 Vue 原始碼解析(例項化前) - 初始化全域性API(一) 這裡做過詳細的講解,有不瞭解的朋友,可以跳轉這裡去看;

做一個渲染攔截,這裡的攔截,最主要是為了在呼叫 render 方法的時候,通過 vm.$createElement 方法進行 dom 的建立;

function initLifecycle (vm) {
  var options = vm.$options;

  // 找到第一個非抽象父級
  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;
}
複製程式碼

初始化了一些引數;

function initEvents (vm) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // init父級附加事件
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}
function updateComponentListeners (
  vm,
  listeners,
  oldListeners
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
  target = undefined;
}
複製程式碼

初始化事件,如果 _parentListeners 存在的話,更新元件的事件監聽;

function initRender (vm) {
  vm._vnode = null; // 子樹的根
  vm._staticTrees = null; // v-once快取的樹
  var options = vm.$options;
  var parentVnode = vm.$vnode = options._parentVnode; // 父樹中的佔位符節點
  var renderContext = parentVnode && parentVnode.context;
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  vm.$scopedSlots = emptyObject;
  // 將createElement fn繫結到此例項,以便我們在其中獲得適當的渲染上下文。
  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以便更容易建立HOC。
  // 他們需要被動反應,以便使用它們的HOC始終更新
  var parentData = parentVnode && parentVnode.data;

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
      !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
    }, true);
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {
      !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
    }, true);
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true);
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true);
  }
}
複製程式碼

初始化渲染,defineReactive 的使用和作用,在 Vue 原始碼解析(例項化前) - 響應式資料的實現原理 這裡有講解,大家想了解可以看一下;

到了這裡執行完畢後,就呼叫到了 beforeCreate 方法。

created(初始化介面後)

initInjections(vm); // 在資料/道具之前解決注入
initState(vm);
initProvide(vm); // 解決後提供的資料/道具
callHook(vm, 'created');
複製程式碼
function resolveInject (inject, vm) {
  if (inject) {
    // 因為流量不足以弄清楚快取
    var result = Object.create(null);
    var keys = hasSymbol
      ? Reflect.ownKeys(inject).filter(function (key) {
        return Object.getOwnPropertyDescriptor(inject, key).enumerable
      })
      : Object.keys(inject);

    for (var i = 0; i < keys.length; i++) {
      var key = keys[i];
      var provideKey = inject[key].from;
      var source = vm;
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey];
          break
        }
        source = source.$parent;
      }
      if (!source) {
        if ('default' in inject[key]) {
          var provideDefault = inject[key].default;
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault;
        } else if (process.env.NODE_ENV !== 'production') {
          warn(("Injection \"" + key + "\" not found"), vm);
        }
      }
    }
    return result
  }
}
var shouldObserve = true;

function toggleObserving (value) {
  shouldObserve = value;
}
function initInjections (vm) {
  var result = resolveInject(vm.$options.inject, vm);
  if (result) {
    toggleObserving(false);
    Object.keys(result).forEach(function (key) {
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(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
          );
        });
      } else {
        defineReactive(vm, key, result[key]);
      }
    });
    toggleObserving(true);
  }
}
複製程式碼

在這裡,其實最主要就是用來做不需要響應式的資料,官方文件:provide / inject

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);
  }
}
複製程式碼

在處理完 inject 後,緊接著就做了 propsmethodsdatacomputedwatch 的初始化處理;

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

ProvideInject 作用其實是一樣的,只是處理的方式不一樣,具體區別請看官方文件:provide / inject

到這裡執行完畢後,就要走到 created 鉤子了。

beforeMount(渲染dom前)

if (vm.$options.el) {
  vm.$mount(vm.$options.el);
}
複製程式碼

在渲染 dom ,先檢查了是否存在渲染位置,如果不存在的話,也就不會註冊了;

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;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
  }
  callHook(vm, 'beforeMount');
}
複製程式碼

beforeMount 這裡,基本沒做什麼事情,只是做了一個 render 方法如果存在就繫結一下 createEmptyVNode 函式;

繫結完畢後,就執行了 beforeMount 鉤子;

mounted(渲染dom後)

  var updateComponent;
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = function () {
      var name = vm._name;
      var id = vm._uid;
      var startTag = "vue-perf-start:" + id;
      var endTag = "vue-perf-end:" + id;

      mark(startTag);
      var vnode = vm._render();
      mark(endTag);
      measure(("vue " + name + " render"), startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating);
      mark(endTag);
      measure(("vue " + name + " patch"), startTag, endTag);
    };
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }

  // 我們在觀察者的建構函式中將其設定為vm._watcher,因為觀察者的初始補丁可能會呼叫$ forceUpdate(例如,在子元件的掛載掛鉤內),這依賴於已定義的vm._watcher
  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
  hydrating = false;

  // 手動掛載的例項,在自己掛載的呼叫掛載在其插入的掛鉤中為渲染建立的子元件呼叫
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
複製程式碼

new Watcher 的時候,呼叫了 _render 方法,實現了 dom 的渲染,具體 _render 都做了什麼,點選檢視 vue 原始碼解析(例項化前) - 初始化全域性 API(最終章)

在執行完例項化 Watcher 以後,如果 $node 不存在,就說明是初始化渲染,執行 mounted 鉤子;

beforeUpdate(更新資料前)

Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate');
    }

};
複製程式碼

如果當前的 vue 例項的 _isMountedtrue 的話,直接呼叫 beforeUpdate 鉤子;

_isMounted 在 mounted 鉤子執行前就已經設定為 true 了。

執行 beforeUpdate 鉤子;

updated(更新資料後)

function callUpdatedHooks (queue) {
  var i = queue.length;
  while (i--) {
    var watcher = queue[i];
    var vm = watcher.vm;
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated');
    }
  }
}
複製程式碼

因為有多個元件的時候,會有很多個 watcher ,在這裡,就是檢查當前的得 watcher 是哪個,是當前的話,就直接執行當前 updated 鉤子。

beforeDestroy(解除安裝元件前)

Vue.prototype.$destroy = function () {
    var vm = this;
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy');
};
複製程式碼

在解除安裝前,檢查是否已經被解除安裝,如果已經被解除安裝,就直接 return 出去;

執行 beforeDestroy 鉤子;

destroyed(解除安裝元件後)

vm._isBeingDestroyed = true;
// 從父級那裡刪除自己
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--;
}
// 準備執行最後一個鉤子
vm._isDestroyed = true;
// 在當前渲染的樹上呼叫destroyed hook
vm.__patch__(vm._vnode, null);

callHook(vm, 'destroyed');
複製程式碼

其實這裡就是把所有有關自己痕跡的地方,都給刪除掉;

執行 destroyed 鉤子。

總結

到這裡,其實每一個生命週期的鉤子做了什麼,我們已經瞭解的差不多了,那這樣大量的程式碼看起來可能不是很方便,所以我們做一個總結的 list

  • beforeCreate :初始化了部分引數,如果有相同的引數,做了引數合併,執行 beforeCreate
  • created :初始化了 InjectProvidepropsmethodsdatacomputedwatch,執行 created
  • beforeMount :檢查是否存在 el 屬性,存在的話進行渲染 dom 操作,執行 beforeMount
  • mounted :例項化 Watcher ,渲染 dom,執行 mounted
  • beforeUpdate :在渲染 dom 後,執行了 mounted 鉤子後,在資料更新的時候,執行 beforeUpdate
  • updated :檢查當前的 watcher 列表中,是否存在當前要更新資料的 watcher ,如果存在就執行 updated
  • beforeDestroy :檢查是否已經被解除安裝,如果已經被解除安裝,就直接 return 出去,否則執行 beforeDestroy
  • destroyed :把所有有關自己痕跡的地方,都給刪除掉;

結束語

Vue 生命週期實現,就先講到這裡了,裡面有些地方,細節講的不是很多,因為這個文章和之前的原始碼解析方向和目的不一樣,原始碼講解的目的是為了讓大家一步一步的去了解,都寫了什麼,而這篇文章的目的是為了讓大家瞭解到每個生命週期的階段,都做了什麼。

如果大家有覺得有問題的地方,或者寫的不好的地方,還請直接下方評論指出,謝謝了。

相關文章