Vue 原始碼解析(例項化前) - 初始化全域性API(一)

熱情的劉大爺發表於2019-01-10

前言

之前,我們在網上,可以看到很多有關vue部分功能的實現原理,尤其是資料雙向繫結那一塊的,文章很多,但是都是按照同樣的思想去實現的一個資料雙向繫結的功能,但不是vue的原始碼。

今天,我在一行一行的去看vue的所有程式碼,並挨個作出解釋,這個時候我們可以發現,vue的細節,很值得我們去學習。

大家覺得寫的有用的話,幫忙點個關注,點點贊,有問題可以評論,只要我看到,我會第一時間回覆。

話不多說,直接開始了。

正文

初始化

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方法的所有程式碼,行數不多,但是知識點很多。

var configDef = {};
複製程式碼

這個函式宣告瞭一個configDef得空物件;

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

然後在給configDef新增了一個get屬性,這個屬性返回得是一個config物件,這個cofig物件裡面,有n個屬性,下面來一一解釋一下:

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 / options

預設值: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原始碼解析的起始篇,接下來我會持續更新該系列的文章,歡迎大家批評和點評,還是老話,多點關注,多點贊?

謝謝大家。

相關文章