「試着讀讀 Vue 源代碼」new Vue()發生了什麼 ❓

說明

  • 首先這篇文章是讀 vue.js 源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0 發佈前深刻的瞭解其原理。javascript

  • 若是你從未看過或者接觸過 vue.js 源代碼,建議你參考如下列出的 vue.js 解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo 演示某一功能點或 API 實現,力求簡要梳理過程。html

  • 若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )vue

  • 文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17java

  • 若有任何疏漏和錯誤之處歡迎指正、交流。node

Vue 構造函數

/** * Vue構造函數 * * @param {*} options 選項參數 */
function Vue(options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue是一個構造函數,應該用「new」關鍵字調用');
  }
  this._init(options);
}
複製代碼

咱們知道 new Vue()將執行 Vue 構造函數, 進而執行 _init(), 那 _init 方法從何處而來?答案是Vue在初始化時添加了該方法,若是你對初始化還不是很清楚,建議你參考上文對初始化過程的梳理性文章:「試着讀讀 Vue 源代碼」初始化先後作了哪些事情❓git

_init()

import config from '../config';
import { initProxy } from './proxy';
import { initState } from './state';
import { initRender } from './render';
import { initEvents } from './events';
import { mark, measure } from '../util/perf';
import { initLifecycle, callHook } from './lifecycle';
import { initProvide, initInjections } from './inject';
import { extend, mergeOptions, formatComponentName } from '../util/index';

let uid = 0;
export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function(options?: Object) {
    const vm: Component = this; // 當前 Vue 實例
    vm._uid = uid++; // 當前 Vue 實例惟一標識

    /**************************** 非生產環境下進行性能監控 --- start ****************************/
    let startTag, endTag;
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`;
      endTag = `vue-perf-end:${vm._uid}`;
      mark(startTag);
    }

    vm._isVue = true; // 一個標誌,避免該對象被響應系統觀測

    /****************** 對 Vue 提供的 props、data、methods等選項進行合併處理 ******************/
    // _isComponent 內部選項:在 Vue 建立組件的時候纔會生成
    if (options && options._isComponent) {
      initInternalComponent(vm, options); // 優化內部組件實例化,由於動態選項合併不是常慢,並且沒有一個內部組件選項須要特殊處理。
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // parentVal
        options || {}, // childVal
        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'); // 調用生命週期鉤子函數 -- beforeCreate
    initInjections(vm); // resolve injections before data/props
    initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created'); // 此時尚未任何掛載的操做,因此在 created 中是不能訪問DOM的,即不能訪問 $el

    /**************************** 非生產環境下進行性能監控 --- end ****************************/
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(`vue ${vm._name} init`, startTag, endTag);
    }

    /**************************** 根據掛載點,調用掛載函數 ****************************/
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}
複製代碼
  • 根據_init方法所作的事情可大概梳理出如下要點:
    • ① 在非生產環境下開啓性能監控程序(監控 ②、③、④ 執行過程耗時)。
    • ② 對 Vue 提供的 props、data、methods 等選項進行合併處理。
    • ③ 設置渲染函數的做用域代理。
    • ④ 執行相關初始化程序及調用初期生命週期函數。
    • ⑤ 根據掛載點,調用掛載函數。

注:性能監控:利用 Web Performance API 容許網頁訪問某些函數來測量網頁和Web應用程序的性能; 這裏是Vue - mark、measure具體代碼實現,就不過多贅述了; 接下來着重看被監控的幾個步驟主要作了什麼?github

new Vue()

若是就單單看代碼,可能就不太直觀且不易理解;不如直接用 Demo 代入斷點調試看看每一步是如何作的,那將會使你對代碼的運行有更直觀的理解與認識。數組

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>vue.js DEMO</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>計算屬性:{{messageTo}}</p>
      <p>數據屬性:{{ message }}</p>
      <button @click="update">更新</button>
      <item v-for="item in list" :msg="item" :key="item" @rm="remove(item)" />
    </div>

    <script> new Vue({ el: '#app', components: { item: { props: ['msg'], template: `<div style="margin-top: 20px;">{{ msg }} <button @click="$emit('rm')">x</button></div>`, created() { console.log('---componentA - 組件生命週期鉤子執行 created---'); } } }, mixins: [ { created() { console.log('---created - mixins---'); }, methods: { remove(item) { console.log('響應移除:', item); } } } ], data: { message: 'hello vue.js', list: ['hello,', 'the updated', 'vue.js'], obj: { a: 1, b: { c: 2, d: 3 } } }, computed: { messageTo() { return `${this.message} !;`; } }, watch: { message(val, oldVal) { console.log(val, oldVal, 'message - 改變了'); } }, methods: { update() { this.message = `${this.list.join(' ')} ---- ${Math.random()}`; } } }); </script>
  </body>
</html>
複製代碼

根據上述 demo 斷點進入 Vue 構造函數 options 參數以下斷點圖所:app

選項合併處理

  • 根據上述 Demo 咱們着重分析執行代碼即 mergeOptions函數,根據代碼可知該函數是對咱們傳入的options作了一層處理,而後賦值給實例屬性$optionsdom

  • resolveConstructorOptions, 該函數主要判斷構造函數是否存在父類,若存在父類須要對 vm.constructor.options 進行處理返回,若不存在直接返回vm.constructor.options; 根據上述Demo直接返回 vm.constructor.options

  • 注:在上文初始化過程對 vm.constructor.options 進行處理,其結果爲:

    Vue.options = {
      components: {
        KeepAlive,
        Transition,
        TransitionGroup
      },
      directives: {
        model,
        show
      },
      filters: Object.create(null),
      _base: Vue
    };
    複製代碼
// _isComponent 內部選項:在 Vue 建立組件的時候纔會生成
if (options && options._isComponent) {
  initInternalComponent(vm, options); // 優化內部組件實例化,由於動態選項合併不是常慢,並且沒有一個內部組件選項須要特殊處理。
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor), // parentVal
    options || {}, // childVal
    vm
  );
}
複製代碼

根據上述分析,程序進入 mergeOptions 函數內部,下面斷點圖展現了該函數的入參:

mergeOptions

將兩個 option 對象合併到一個新的 options,用於實例化和繼承的核心實用程序中。

export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object {
  // 校驗組件的名字是否符合要求:
  // 限定組件的名字由普通的字符和中橫線(-)組成,且必須以字母開頭。
  // 檢測是不是內置的標籤(如:slot) || 檢測是不是保留標籤(html、svg等)。
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child);
  }

  // 若是 child 是一個函數的話,去其靜態屬性 options 重寫 child;
  if (typeof child === 'function') {
    child = child.options;
  }

  /************************ 規範化處理 ************************/
  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirectives(child);

  /************************ extends/mixins 遞歸處理合並 ************************/
  const extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
  }

  /************************ 合併階段 ************************/
  const options = {};
  let key;
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField(key) {
    const strat = strats[key] || defaultStrat;
    options[key] = strat(parent[key], child[key], vm, key);
  }

  return options;
}
複製代碼

規範化處理

normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
複製代碼

上述代碼主要對 Vue 選項進行規範化處理,咱們知道 Vue 的選項支持多種寫法,但最終都須要化爲統一格式,進行處理。 下面所列出的是各類寫法與規範化以後的對比; 上述代碼實現就不過多論述了,可直接根據上述導航到代碼段去看便可。

  • Props:

    • 以下幾種寫法:
      • props: ['size', 'myMessage']
      • props: { height: Number }
      • props: { height: { type: Number, default: 0 } }
    • 統一格式處理以後爲:
      • props: { size: { type: null }, myMessage: { type: null } }
      • props: { height: { type: Number } }
      • props: { height: { type: Number, default: 0 } }
  • Inject:

    • 以下幾種寫法:
      • inject: ['foo'],
      • inject: { bar: 'foo' }
    • 統一格式處理以後爲:
      • inject: { foo: { from: 'foo' } }
      • inject: { bar: { from: 'foo' } }
  • Directives:

    • 以下幾種寫法:
      • directives: { foo: function() { console.log('自定義指令: v-foo') }
    • 統一格式處理以後爲:
      • directives: { foo: { bind: function() { console.log('v-foo'), update: function() { console.log('v-foo') } } }

合併階段

代碼到執行到這裏,將開始真正的合併了,最終返回合併以後的options

const options = {};
let key;
for (key in parent) {
  mergeField(key);
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key);
  }
}
function mergeField(key) {
  const strat = strats[key] || defaultStrat;
  options[key] = strat(parent[key], child[key], vm, key);
}
return options;
複製代碼

這裏特別說明一下,Vue 爲每個選項合併都提供了選項合併的策略函數,strats 變量存放着這些函數。這裏就不分別對每一個策略函數進行展開論述了。

const defaultStrat = function(parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal;
};

export function mergeDataOrFn( parentVal: any, childVal: any, vm?: Component ): ?Function {
  // ...
}

// optionMergeStrategies: Object.create(null),
const strats = config.optionMergeStrategies;

// el / propsData 合併策略函數
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function(parent, child, vm, key) {
    // ...
  };
}

// data 合併策略函數
strats.data = function( parentVal: any, childVal: any, vm?: Component ): ?Function {
  // ...
};

// watch 合併策略函數
strats.watch = function( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
  // ...
};

// props、methods、inject、computed 合併策略函數
strats.props = strats.methods = strats.inject = strats.computed = function( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object {
  // ...
};

// provide 合併策略函數
strats.provide = mergeDataOrFn;
複製代碼

根據上述分析, mergeOptions 函數將返回規範化,且合併以後options,下面斷點圖展現了合併以後的options

執行相關初始化程序及調用初期生命週期函數

initLifecycle(vm); // 初始化生命週期
initEvents(vm); // 初始化事件
initRender(vm); // 初始化渲染
callHook(vm, 'beforeCreate'); // 調用生命週期鉤子函數 -- beforeCreate
initInjections(vm); // resolve injections before data/props
initState(vm); // 初始化 initProps、initMethods、initData、initComputed、initWatch
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); // 此時尚未任何掛載的操做,因此在 created 中是不能訪問DOM的,即不能訪問 $el
複製代碼

initLifecycle

  • 以下代碼主要作了:
    • 找到第一個非抽象父級
    • 將當前實例添加到父實例的 $children 屬性裏
    • 並設置當前實例的 $parent 爲父實例
    • 在當前實例上設置一些屬性
export function initLifecycle(vm: Component) {
  const options = vm.$options;
  /** * abstract - 是不是抽象組件 * 抽象組件: 它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中。(如 keep-alive transition ) */
  let 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;
}
複製代碼

initEvents

export function initEvents(vm: Component) {
  // 在當前實例添加 `_events` `_hasHookEvent` 屬性
  vm._events = Object.create(null);
  vm._hasHookEvent = false; // 用於判斷是否存在生命週期鉤子的事件偵聽器
  const listeners = vm.$options._parentListeners; // 初始化父附加事件
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}
複製代碼

initRender

export function initRender(vm: Component) {
  vm._vnode = null; // the root of the child tree
  vm._staticTrees = null; // v-once cached trees

  /*************************** 解析並處理 slot **************************/
  const options = vm.$options;
  const parentVnode = (vm.$vnode = options._parentVnode); // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context;
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  vm.$scopedSlots = emptyObject;

  /*************************** 包裝 createElement() **************************/
  // render: (createElement: () => VNode) => VNode createElement
  // 將createElement fn綁定到這個實例,以便在其中得到適當的呈現上下文。
  // args順序:標籤、數據、子元素、normalizationType、alwaysNormalize內部版本由模板編譯的呈現函數使用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
  // 規範化老是應用於公共版本,用於用戶編寫的呈現函數。
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);

  /*************************** 在實例添加 $attrs/$listeners **************************/
  // $attrs和$listeners 用於更容易的臨時建立。它們須要是反應性的,以便使用它們的 HOC 老是被更新
  const parentData = parentVnode && parentVnode.data;
  if (process.env.NODE_ENV !== 'production') {
    // 定義響應式的屬性
    defineReactive(
      vm,
      '$attrs',
      (parentData && parentData.attrs) || emptyObject,
      () => {
        !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
      },
      true
    );
    defineReactive(
      vm,
      '$listeners',
      options._parentListeners || emptyObject,
      () => {
        !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
    );
  }
  /*************************** 在實例添加 $attrs/$listeners **************************/
}
複製代碼

callHook

export function callHook(vm: Component, hook: string) {
  pushTarget(); // 爲了不在某些生命週期鉤子中使用 props 數據致使收集冗餘的依賴 #7573
  const handlers = vm.$options[hook];
  if (handlers) {
    // 在合併選項處理時:生命週期鉤子選項會被合併處理成一個數組
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm);
      } catch (e) {
        // 捕獲生命週期函數執行過程當中可能拋出的異常
        handleError(e, vm, `${hook} hook`);
      }
    }
  }
  // 判斷是否存在生命週期鉤子的事件偵聽器,在 initEvents 中初始化,若存在觸發響應鉤子函數
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}
複製代碼

這裏額外提一下: 可使用 hook: 加 生命週期鉤子名稱 的方式來監聽組件相應的生命週期

<child @hook:beforeCreate="handleChildBeforeCreate" @hook:created="handleChildCreated" @hook:mounted="handleChildMounted" @hook:生命週期鉤子名稱 />
複製代碼

initInjections

export function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm); // 做用:尋找父代組件提供的數據
  if (result) {
    // provide 和 inject 綁定並非可響應的。
    // 這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。
    toggleObserving(false); // 關閉響應式檢測
    Object.keys(result).forEach(key => {
      // 對每一個屬性定義響應式屬性,並在非生產環境下,提供警告程序。
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `避免直接修改注入的值,由於當提供的組件從新呈現時,更改將被覆蓋。正在修改的注入:「${key}」`,
            vm
          );
        });
      } else {
        defineReactive(vm, key, result[key]);
      }
    });
    toggleObserving(true); // 開啓響應式檢測
  }
}
複製代碼

initState

/** * 初始化 props/ methods/ data/ computed/ watch/ 等選項。 */
export function initState(vm: Component) {
  vm._watchers = [];
  const 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);
  }
}
複製代碼

注: 這裏只是簡單展現了其初始化順序,其內部各個初始化方法將在構建響應式系統深挖。 這裏只須要明白一點,即初始化順序:props => methods => data => computed => watch (根據上述順序,天然也就知道,爲何能夠在data選項中使用props去初始化值)

initProvide

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

上述初始化部分的分析,只是簡單的梳理了其執行過程,若是想對其內部實現作更爲細緻的認識,能夠自行去看看代碼實現或上述說明提到的源碼解析的相關文章。

根據掛載點,調用掛載函數

若存在掛載點,則執行掛載函數,渲染組件。掛載函數如何執行,實現機制如何,將在後文慢慢梳理出來。

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

總結:全文梳理了執行 new Vue() 調用 _init() 方法,接着又跟着代碼執行過程探討了內部實現。


承接上文 - 「試着讀讀 Vue 源代碼」初始化先後作了哪些事❓

承接下文 - 「試着讀讀Vue源代碼」響應式系統是如何構建的❓待續...

相關文章
相關標籤/搜索