前言
之前,我們在網上,可以看到很多有關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原始碼解析的起始篇,接下來我會持續更新該系列的文章,歡迎大家批評和點評,還是老話,多點關注,多點贊?
謝謝大家。