8. Vue的初始化以及生命週期

初始化階段

此階段主要是初始化Vue實例的屬性computed、provide、inject和watch、事件methods以及響應式數據data等 Vue的構造函數爲:vue

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

初始化實例屬性

_init初始化代碼爲:node

Vue.prototype._init = function (options) {
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    );
}
複製代碼

resolveConstructorOptions(vm.constructor)函數的做用是獲取當前當前實例中構造函數的options選項及其全部父級的構造函數的options,而後在_init中由initLifecycle函數初始化實例屬性:git

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

初始化事件

在Vue實例上初始化一些屬性並設置默認值。 接着初始化事件,被初始化的事件就是父組件在模板中使用v-on監聽子組件中觸發的事件:github

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

_parentListeners是父組件傳遞到子組件,其格式以下:vuex

{
    increment: function () {}
}
複製代碼

而後將父組件向子組件註冊的事件註冊到子組件數組

function updateComponentListeners (
    vm,
    listeners,
    oldListeners
  ) {
    target = vm;
    updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
    target = undefined;
}
複製代碼

updateComponentListeners函數具體爲:promise

function updateListeners (
    on,
    oldOn,
    add,
    remove$$1,
    createOnceHandler,
    vm
  ) {
    var name, def$$1, cur, old, event;
    for (name in on) {
      def$$1 = cur = on[name];
      old = oldOn[name];
      // 判斷是否使用修飾符
      event = normalizeEvent(name);
      if (isUndef(cur)) {
        warn(
          "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
          vm
        );
      } else if (isUndef(old)) {
          // 哪些事件在中oldOn不存在,則調用add註冊
        if (isUndef(cur.fns)) {
          cur = on[name] = createFnInvoker(cur, vm);
        }
        if (isTrue(event.once)) {
          cur = on[name] = createOnceHandler(event.name, cur, event.capture);
        }
        add(event.name, cur, event.capture, event.passive, event.params);
      } else if (cur !== old) {
          // 若是on和oldOn不一致,則將事件回調替換成on中的回調,並把on中的回調引用指向oldOn中對應的事件
        old.fns = cur;
        on[name] = old;
      }
    }
    // 刪除在on中不存在的事件
    for (name in oldOn) {
      if (isUndef(on[name])) {
        event = normalizeEvent(name);
        remove$$1(event.name, oldOn[name], event.capture);
      }
    }
}
複製代碼

add函數爲瀏覽器

function add (event, fn, once) {
    if (once) {
        target.$once(event, fn)
    } else {
        target.$on(event, fn)
    }
}
複製代碼

將事件添加到vm._events = {} 中。 initRender函數主要是初始化一些變量屬性和函數緩存

vm._vnode = null; // the root of the child tree
vm._staticTrees = null; // v-once cached trees
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); };
複製代碼

初始化reject和provide

因data或者props中能依賴reject,須要將reject初始化在initState前,inject初始化爲:bash

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

resolveInject函數的做用是經過配置的reject從當前組件自底向上,查找可用的內容

function resolveInject (inject, vm) {
    if (inject) {
      // inject is :any because flow is not smart enough to figure out cached
      var result = Object.create(null);
      var keys = hasSymbol
        ? Reflect.ownKeys(inject)
        : Object.keys(inject);
        // inject中的key值逐級查找,若是全部沒有可用值,則使用默認值
      for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        // #6574 in case the inject object is observed...
        // 跳過響應式的key
        if (key === '__ob__') { continue }
        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 {
            warn(("Injection \"" + key + "\" not found"), vm);
          }
        }
      }
      return result
    }
}
複製代碼

而後初始化數據屬性initState,這個稍後介紹,接着初始化provide

initProvide(vm)

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

其在vm.$options中的形式以下:

_provided: {foo: Window}
複製代碼

初始化數據和方法

剛講到的在初始化reject後,provide前,初始化了data、copmputed、watch、props以及methods等數據方法,先講初始化methods:

initMethods(vm, opts.methods);
複製代碼

其中opts.methods爲

{
    incremenTotal: ƒ incremenTotal(total)
}
複製代碼

首先會判斷props屬性中是否有跟methods的方法名同樣的,有的話則給出警告;再將方法綁定到實例上:

vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
複製代碼

能夠經過vm.x訪問methods中的x了。data、watch以及props前面均有講到,接下來詳細說下computed的原理。

computed原理

computed是一個惰性求值的觀察者,具備緩存性,只有當依賴變化後,第一次訪問computed屬性纔會計算新的值,計算屬性的返回值是否發生變化是經過dirty屬性來肯定,當dirty爲true時,須要從新計算返回值,爲false時不須要從新結算。 初始化computed代碼爲

var computedWatcherOptions = { lazy: true };
function initComputed (vm, computed) {
    // $flow-disable-line

    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    // 計算屬性在SSR環境中,只是一個普通的getter方法
    var isSSR = isServerRendering();
    // 遍歷computed屬性
    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }

      if (!isSSR) {
        // create internal watcher for the computed property.
        // 在非SSR環境下,建立一個watcher觀察器,保存到vm._computedWatchers
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if (!(key in vm)) {
        // 若是計算屬性不在vm中則在vm上設置一個計算屬性
        defineComputed(vm, key, userDef);
      } else {
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
}
複製代碼

若是computed屬性名與methods重名時,計算屬性會失效。defineComputed函數具體爲

function defineComputed (
    target,
    key,
    userDef
  ) {
    // shouldCache爲true表示非SSR環境下
    var shouldCache = !isServerRendering();
    if (typeof userDef === 'function') {
      // 處理函數
      sharedPropertyDefinition.get = shouldCache
        ? createComputedGetter(key)
        : createGetterInvoker(userDef);
      sharedPropertyDefinition.set = noop;
    } else {
      // 處理get對象
      sharedPropertyDefinition.get = userDef.get
        ? shouldCache && userDef.cache !== false
          ? createComputedGetter(key)
          : createGetterInvoker(userDef.get)
        : noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }
    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {
        warn(
          ("Computed property \"" + key + "\" was assigned to but it has no setter."),
          this
        );
      };
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼

在瀏覽器端會執行createComputedGetter(key)函數,以下:

function computedGetter () {
  var watcher = this._computedWatchers && this._computedWatchers[key];
  if (watcher) {
    if (watcher.dirty) {
      watcher.evaluate();
    }
    if (Dep.target) {
      watcher.depend();
    }
    return watcher.value
  }
}
複製代碼

從vm._computedWatchers中取出前面存儲的計算屬性的watcher實例,若是watcher存在,再判斷watcher.dirty,若是爲true,說明計算屬性發生變化,須要從新計算。隨後判斷Dep.target,若是存在,調用watcher.depend(),將計算屬性的watcher添加到相關聯的屬性依賴列表中去。當咱們修改計算屬性中的依賴時,由於組件watcher觀察了計算屬性中的依賴,故當依賴的屬性發生變化時,組件的watcher會獲得通知,而後從新渲染。watcher的相關代碼爲:

//  其中options爲var computedWatcherOptions = { lazy: true };
var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    if (options) {
      this.lazy = !!options.lazy; // true
      this.sync = !!options.sync; // false
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    ...
    this.value = this.lazy
      ? undefined
      : this.get();
}
Watcher.prototype.evaluate = function evaluate () {
    // 從新計算下值
    this.value = this.get();
    this.dirty = false;
  };
Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    // 遍歷this.deps依賴列表,將computed的watcher添加到相關的列表中
    while (i--) {
      this.deps[i].depend();
    }
};
複製代碼

執行dep的depend方法能將組件的watcher添加到依賴列表,當這些狀態發生變化時,組件的watcher也會收到通知,並進行從新渲染。

模板編譯階段

該階段主要在created和beforeMount鉤子函數之間,這個階段解析模板,生成渲染函數,只存在完整版中.

掛載階段

在beforeMount和mounted鉤子函數之間,將生成的render函數生成VNode,並掛載到頁面,在掛載過程當中,Watcher會持續追蹤狀態變化

卸載階段

執行vm.$destroy方法,Vue.js會將自身從父組件中刪掉,取消實例上全部的依賴追蹤而且移除全部的事件監聽器

番外篇

$nextTick的原理

vm.$nextTick和Vue.nextTick方法是一致的,都是被抽象成了nextTick方法:

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};
複製代碼

nextTick函數的具體實現方式以下:

function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    // $flow-disable-line
    // 用於沒有回調函數返回Promise,支持nextTick().then形式
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
}
複製代碼

callbacks數組用來存儲用戶註冊的回調,pending表示是否須要被添加入任務隊列,接着執行timerFunc函數:

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) { setTimeout(noop); }
    };
    isUsingMicroTask = true;
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Techinically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
}
複製代碼

timerFunc表示一個異步延遲包裝器,保延遲調用 flushCallbacks 函數。Vue2.6.*版本以微任務優先,其順序爲promise > MutationObserver > setImmediate > setTimeout。Vue2.5.*的總體優先級是:Promise > setImmediate > MessageChannel > setTimeout,flushCallbacks函數是遍歷執行回調函數

function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
      copies[i]();
    }
}
複製代碼

DOM的異步更新

當$nextTick的回調函數改變了狀態值,會觸發Object.defineProperty中的set屬性,隨後執行

dep.notify();
// Dep類的notify方法
Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; 複製代碼

觸發watcher的update方法:

Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    // computed須要更新值時
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      // 同步執行時
      this.run();
    } else {
      // 加入到任務隊列中
      queueWatcher(this);
    }
};
複製代碼

queueWatcher函數主要執行nextTick(flushSchedulerQueue)函數,表示下一個事件循環執行flushSchedulerQueue函數,該函數簡化爲:

function flushSchedulerQueue () {
    flushing = true
    let watcher, id

    for (index = 0; index < queue.length; index++) {
      watcher = queue[index]
      if (watcher.before) {
        watcher.before()
      }
      id = watcher.id
      has[id] = null
      watcher.run()
}
複製代碼

此時能更新視圖,從新渲染。

Vue的錯誤處理機制

Vue的API中有個errorCapture鉤子函數的做用是捕獲來自子孫組件的錯誤,當返回false時,組織錯誤繼續傳播,其錯誤傳播機制以下:

  • 默認狀況下,若是全局的 config.errorHandler定義,全部的錯誤仍會發送它,所以這些錯誤仍然會向單一的分析服務的地方進行彙報
  • 若是一個組件的繼承或父級從屬鏈路中存在多個 errorCaptured 鉤子,則它們將會被相同的錯誤逐個喚起。
  • 若是此 errorCaptured 鉤子自身拋出了一個錯誤,則這個新錯誤和本來被捕獲的錯誤都會發送給全局的 config.errorHandler,不能捕獲異步promise內部拋出的錯誤和自身的錯誤
  • 一個 errorCaptured 鉤子可以返回 false 以阻止錯誤繼續向上傳播 全局config.errorHandler具體用法以下:
Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的錯誤信息,好比錯誤所在的生命週期鉤子
  // 只在 2.2.0+ 可用
}
複製代碼

相關代碼以下:

function handleError (err, vm, info) {
    // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
    // See: https://github.com/vuejs/vuex/issues/1505
    pushTarget();
    try {
      if (vm) {
        var cur = vm;
        // 自底向上循環讀取父組件errorCaptured鉤子函數,並執行
        while ((cur = cur.$parent)) {
          var hooks = cur.$options.errorCaptured;
          if (hooks) {
            for (var i = 0; i < hooks.length; i++) {
              try {
                var capture = hooks[i].call(cur, err, vm, info) === false;
                // 若是errorCaptured鉤子函數返回的值是false,則直接返回
                if (capture) { return }
              } catch (e) {
                globalHandleError(e, cur, 'errorCaptured hook');
              }
            }
          }
        }
      }
      globalHandleError(err, vm, info);
    } finally {
      popTarget();
    }
  }
複製代碼

而後執行globalHandleError(err, vm, info)函數:

function globalHandleError (err, vm, info) {
    if (config.errorHandler) {
      try {
        // 傳遞到全局的errorHandler函數處理
        return config.errorHandler.call(null, err, vm, info)
      } catch (e) {
        // if the user intentionally throws the original error in the handler,
        // do not log it twice
        if (e !== err) {
          logError(e, null, 'config.errorHandler');
        }
      }
    }
    // 打印錯誤
    logError(err, vm, info);
  }

複製代碼
相關文章
相關標籤/搜索