Vue 源碼解析(實例化前) - 初始化全局API(一)

前言

以前,咱們在網上,能夠看到不少有關vue部分功能的實現原理,尤爲是數據雙向綁定那一塊的,文章不少,可是都是按照一樣的思想去實現的一個數據雙向綁定的功能,但不是vue的源碼。javascript

今天,我在一行一行的去看vue的全部代碼,並挨個做出解釋,這個時候咱們能夠發現,vue的細節,很值得咱們去學習。vue

你們以爲寫的有用的話,幫忙點個關注,點點贊,有問題能夠評論,只要我看到,我會第一時間回覆。java

話很少說,直接開始了。ios

正文

初始化

initGlobalAPI(Vue);
複製代碼

這個時候,初始化調用initGlobalAPI,傳入Vue構造函數。這裏是在Vue構造函數實例化以前要作的事情,因此這裏先不講Vue對象裏面作了什麼,先講實例化以前作了什麼。編程

function initGlobalAPI (Vue) {
  // config
  var configDef = {};
  configDef.get = function () { return config; };
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = function () {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      );
    };
  }
  Object.defineProperty(Vue, 'config', configDef);

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive
  };

  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;

  Vue.options = Object.create(null);
  ASSET_TYPES.forEach(function (type) {
    Vue.options[type + 's'] = Object.create(null);
  });

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue;

  extend(Vue.options.components, builtInComponents);

  initUse(Vue);
  initMixin$1(Vue);
  initExtend(Vue);
  initAssetRegisters(Vue);
}
複製代碼

這是initGlobalAPI方法的全部代碼,行數很少,可是知識點不少。api

var configDef = {};
複製代碼

這個函數聲明瞭一個configDef得空對象;數組

configDef.get = function () { return config; };
複製代碼

而後在給configDef添加了一個get屬性,這個屬性返回得是一個config對象,這個cofig對象裏面,有n個屬性,下面來一一解釋一下:less

config對象

var config = ({
  optionMergeStrategies: Object.create(null),
  silent: false,
  productionTip: process.env.NODE_ENV !== 'production',
  devtools: process.env.NODE_ENV !== 'production',
  performance: false,
  errorHandler: null,
  warnHandler: null,
  ignoredElements: [],
  keyCodes: Object.create(null),
  isReservedTag: no,
  isReservedAttr: no,
  isUnknownElement: no,
  getTagNamespace: noop,
  parsePlatformTagName: identity,
  mustUseProp: no,
  _lifecycleHooks: LIFECYCLE_HOOKS
})
複製代碼

optionMergeStrategies:選項合併,用於合併core / util / optionside

默認值:object.creart(null)函數式編程

注:object.creart(null)去建立的一個是原子,什麼是原子呢,就是它是對象,可是不繼承Object() ,這裏對原子的概念不作深究,你們若是感興趣,能夠百度去查「js元系統」,aimingoo對這方面有作過詳細的說明。

silent:是否取消警告

默認值:false

productionTip:項目啓動時,是否顯示提示信息

默認值:process.env.NODE_ENV !== 'production'

若是是開發環境,則是true,表示顯示提示信息,在生產環境則不顯示

devtools:是否啓用devtools

默認值:同productionTip

performance:是否記錄性能

默認值:false

errorHandler:觀察程序錯誤的錯誤處理程序

默認值:null

warnHandler:觀察程序警告的警告處理程序

默認值:null

ignoredElements:忽略某些自定義元素

默認值:[]

keyCodes:v - on的自定義用戶keyCode

默認值:object.creart(null)

isReservedTag:檢查是否保留了標記,以便它不能註冊爲組件。這取決於平臺,可能會被覆蓋

var no = function (a, b, c) { return false; };
複製代碼

默認值:一個名爲no的function,這個function接收三個參數,可是結果永遠返回的是false

isReservedAttr:檢查屬性是否被保留,以便不能用做組件道具。這取決於平臺,可能會被覆蓋

默認值:同上

isUnknownElement:檢查標記是否爲未知元素。取決於平臺

默認值:同上

getTagNamespace:獲取元素的命名空間

function noop (a, b, c) {}
複製代碼

默認值:一個名爲noop的函數,裏面什麼都沒有作

parsePlatformTagName:解析特定平臺的真實標籤名稱

var identity = function (_) { return _; };
複製代碼

默認值:一個名爲identity的函數,輸入的什麼就輸出的什麼

mustUseProp:檢查是否必須使用屬性(例如值)綁定屬性。這個取決於平臺

默認值:一個名爲no的function

_lifecycleHooks:生命週期鉤子數組

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

默認值:一個數組,裏面有全部生命週期的方法名

以上就是config裏面全部的屬性

config.set

if (process.env.NODE_ENV !== 'production') {
    configDef.set = function () {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      );
    };
  }
複製代碼

作了一個判斷是不是生產環境,若是不是生產環境,給configDef添加一個set方法

Object.defineProperty(Vue, 'config', configDef);
複製代碼

在這裏,爲Vue的構造函數,添加一個要經過Object.defineProperty監聽的屬性config,獲取的時候,獲取到的是上面描述的那個config對象,若是對這個config對象直接作變動,就會提示「不要替換vue.config對象,而是設置單個字段」,說明,做者不但願咱們直接去替換和變動整個config對象,若是有須要,但願去直接修改咱們須要修改的值

公開util

Vue.util = {
    warn: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive
};
複製代碼

在這裏,設置了一個公開的util對象,可是它不是公共的api,避免依賴,除非你意識到了風險,下面來介紹一下它的屬性:

warn:警示
var warn = noop;
var generateComponentTrace = (noop);
if (process.env.NODE_ENV !== 'production') {
    warn = function (msg, vm) {
        var trace = vm ? generateComponentTrace(vm) : '';
        if (config.warnHandler) {
          config.warnHandler.call(null, msg, vm, trace);
        } else if (hasConsole && (!config.silent)) {
          console.error(("[Vue warn]: " + msg + trace));
        }
  };
}
複製代碼

warn是一個function,初始化的時候,只定義了一個noop方法,若是在開發環境,這個warn是能夠接收兩個參數,一個是msg,一個是vm,msg不用說,你們都知道這裏是一個提示信息,vm就是實例化的vue對象,或者是實例化的vue對象的某一個屬性。

接下來是一個三元表達式trace,用來判斷調用warn方法時,是否有傳入了vm,若是沒有,返回的是空,若是有,那麼就返回generateComponentTrace這個function,這個方法初始化的值也是noop,什麼都沒有作,目的是解決流量檢查問題

若是config.warnHandler被使用者變動成了值,不在是null,那麼就把config.warnHandler的this指向null,其實就是指向的window,再把msg, vm, trace傳給config.warnHandler

不然判斷當前環境使用支持conosle而且開啓了警告,若是開啓了,那就把警告提示信息打印出來

extend:繼承
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}
複製代碼

這個方法是用於作繼承操做的,接收兩個值to, _from,將屬性_from混合到目標對象to中,若是to存在_from中的屬性,則直接覆蓋,最後返回新的to

mergeOptions:將兩個選項對象合併爲一個新對象,用於實例化和繼承的核心實用程序(這是一個很重要的方法,在後面多處會用到,因此建議你們仔細看這裏)
function mergeOptions (parent, child, vm) {

  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child);
  }

  if (typeof child === 'function') {
    child = child.options;
  }

  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirecitives(child);
  var extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  if (child.mixins) {
    for (var i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
  }
  var options = {};
  var key;
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField (key) {
    var strat = strats[key] || defaultStrat;
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options
}
複製代碼
if (process.env.NODE_ENV !== 'production') {
    checkComponents(child);
}
function checkComponents (options) {
  for (var key in options.components) {
    validateComponentName(key);
  }
}
function validateComponentName (name) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    );
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    );
  }
}
複製代碼

這個方法接收三個參數parent,child,vm,在不是生產環境的狀況下,會去檢測參數child中,是否存在components,若是存在該對象,遍歷全部的componets,進行名稱是否符合規範,這裏有一個正則,是用來判斷以字母開頭,以0個或多個任意字母和字符「-」結尾的字符串,若是不符合這個規定的話,就會提示警告信息

if (typeof child === 'function') {
    child = child.options;
}
複製代碼

若是child是一個function的話,則把child本身指向child的options屬性

接下來要作的就是規範child裏面的Props、Inject、Direcitives

normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirecitives(child);
複製代碼
normalizeProps:規範屬性,確保全部的props的規範都是基於對象的
function normalizeProps (options, vm) {
  var props = options.props;
  if (!props) { return }
  var res = {};
  var i, val, name;
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);
        res[name] = { type: null };
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.');
      }
    }
  } else if (isPlainObject(props)) {
    for (var key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val)
        ? val
        : { type: val };
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      "Invalid value for option \"props\": expected an Array or an Object, " +
      "but got " + (toRawType(props)) + ".",
      vm
    );
  }
  options.props = res;
}

複製代碼
var props = options.props;
if (!props) { return }
複製代碼

一開始,會檢查child是否存在props屬性,若是不存在,直接return出去,若是存在的話則是去聲明瞭幾個變量,一個名爲res的對象,還有i, val, name

if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);
        res[name] = { type: null };
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.');
      }
    }
  }
複製代碼

檢查props是數組仍是對象,若是是數組的話,則是去循環它,並判斷每個數組項,是不是字符串,若是是字符串那麼就去執行camelize方法。

camelize:

var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
複製代碼

把名稱格式爲「xx-xx」的變爲「xxXx」,這裏接收的是當前的props屬性值,一個字符串

cached:

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
複製代碼

在調用camelize方法的時候,camelize調用了cached,這是一個暫存式函數,對暫存式函數不瞭解的朋友,能夠去看看函數式編程,在cached也是建立了一個原子cache,而後會返回一個cachedFn方法,這裏會檢測cache是否存在當前props屬性值的屬性,若是存在,直接返回,若是不存在,則是調用,調用cached的方法傳過來的function,在調用cached方法的方法中返回的結果,返回到調用cached方法的方法(這句話我知道很繞口,可是我只會這麼解釋,哪位大佬有更好的表述方式,歡迎評論,我作修改)

而後把全部的數組項,而且是字符串的,所有都遍歷一遍,作這樣的處理,而後在res對象裏面,去添加一個屬性,它是一個對象,屬性名就是遍歷後的這個遍歷後的值(把-轉換成大寫字母),屬性值有一個初始化的type屬性,值爲null

固然不是生產環境下,而且props雖然是數組,可是數組項不是字符串的話,會警告你「使用數組語法時,props必須是字符串」

var _toString = Object.prototype.toString;
function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}
else if (isPlainObject(props)) {
    for (var key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val)
        ? val
        : { type: val };
    }
}
options.props = res;
複製代碼

若是child的props不是數組,使用isPlainObject去判斷props是不是對象,這個方法代碼就一行,很簡單,也比較好理解,我也就不浪費篇幅去解釋了;

若是是對象的話,就去遍歷它,把全部的屬性名按照上面數組項的處理方式,去處理全部的數組名,而且看成res的屬性名,該屬性名的值須要去判斷原props的該屬性的值是不是對象,若是是對象,直接看成當前屬性名的屬性值,若是不是的話,則給當前處理後的屬性名,傳一個對象,type屬性的值就是原props該屬性名的屬性值

這裏,就把child裏面全部的props給規範化了,最後覆蓋了源child的props屬性(這一個方法的內容真多,各類知識點,有沒有,點波贊吧)

normalizeInject:規範Inject
function normalizeInject (options, vm) {
  var inject = options.inject;
  if (!inject) { return }
  var normalized = options.inject = {};
  if (Array.isArray(inject)) {
    for (var i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] };
    }
  } else if (isPlainObject(inject)) {
    for (var key in inject) {
      var val = inject[key];
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val };
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      "Invalid value for option \"inject\": expected an Array or an Object, " +
      "but got " + (toRawType(inject)) + ".",
      vm
    );
  }
}
複製代碼

和props同樣,先檢查是否存在,不存在直接返回;

若是存在的話,把child的inject存在一個變量inject裏,把child裏面的inject變成空對象,而且把該值傳給一個normalized的變量;

若是inject是一個數組的話,則遍歷它,normalized的每個屬性名,就是每個inject的數組項,每個屬性值都是一個對象,對象的屬性from的值,就是每個inject的數組項

若是inject是一個對象的話,則遍歷它,把每個屬性值存爲變量val,normalized的key,就是inject的key,若是val是一個對象的話,則把{ from: key }和val合併,val覆蓋{ from: key }

normalizeDirectives:規範Directives
function normalizeDirectives (options) {
  var dirs = options.directives;
  if (dirs) {
    for (var key in dirs) {
      var def = dirs[key];
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def };
      }
    }
  }
}
複製代碼

源碼裏只處理了child.directives的對象格式,若是存在的話遍歷它,若是每個屬性值def都是function的話則把每個directives的屬性值改成{ bind: def, update: def };

到這裏,規範化的事情就作完了,休息一下,點個關注點個贊,我們繼續。

var extendsFrom = child.extends;
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm);
}
複製代碼

看child是否存在extends,遞歸當前的mergeOptions方法,parent就是當前的parent,child就是當前child的extends的值;

if (child.mixins) {
    for (var i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
}
複製代碼

檢測child是否存在mixins,若是存在的話,遞歸當前的mergeOptions方法,並把最新的結果,去覆蓋上一次調用mergeOptions方法的parent;

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

var strats = config.optionMergeStrategies;//這只是初始化的值

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

如今聲明瞭一個options的對象,而後分別去遍歷了parent和child,parent和child的key傳給了一個mergeField的方法;

在mergeField中聲明一個start變量,若是strats下的存在當前這個key的屬性,則返回,不然就返回一個默認的defaultStrat;

defaultStrat接收兩個參數,第一個參數是parent,第二個是child,若是child存在就返回child,不然就返回parent;

把mergeField接收到的key,看成以前optins的key,它的值就是前面返回的變量start方法返回的值;

最後,把整個options返回。

結束語

到這裏,Vue.util的四個屬性已經講了三個了,第四個屬性是一個defineReactive方法,我不打算在這一篇去講,由於這個方法,就是實現一個數據雙向綁定的核心方法,內容可能會比較多,並且這一篇的內容也已經夠長了,寫的再多的話,不適合學習了,因此我打算在下一篇單獨去講一下defineReactive這個方法。

這篇文章,是vue源碼解析的起始篇,接下來我會持續更新該系列的文章,歡迎你們批評和點評,仍是老話,多點關注,多點贊😊

謝謝你們。

相關文章
相關標籤/搜索