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

謝謝大家。

來源:https://juejin.im/post/5c371fd76fb9a049e23236ad

相關文章