雖然vue3已經(jīng)出來很久了,但我覺得vue.js的源碼還是非常值得去學習一下僧家。vue.js里面封裝的很多工具類在我們平時工作項目中也會經(jīng)常用到八拱。所以我近期會對vue.js的源碼進行解讀乘粒,分享值得去學習的代碼片段伤塌,這篇文章將會持續(xù)更新每聪。
一、1200~2400代碼有哪些內(nèi)容绑洛?:
1.一系列合并策略:
①選項 el真屯、propsData 的合并策略
strats.el 绑蔫,strats.propsData
②選項data的合并策略
strats.data
③生命周期鉤子選項的合并策略
mergeHoo
④資源(assets)選項的合并策略
mergeAssets
⑤選項 watch 的合并策略
strats.watch
⑥選項 props配深、methods嫁盲、inject羞秤、computed 的合并策略
strats.props锥腻,strats.methods ,strats.inject 京革, strats.computed匹摇,都是使用相同的合并策略函數(shù)
⑦provide 的合并策略,與data的合并策略相同
strats.provide
⑧合并策略函數(shù)
mergeOptions
2.一些屬性的校驗和規(guī)范化
①validateComponentName:組件名的校驗
②normalizeProps:對props的屬性規(guī)范化
③normalizeInject:對inject的屬性規(guī)范化
④normalizeDirectives:對directives的屬性規(guī)范化
3.一些警告信息懈贺,異常捕獲梭灿,異常處理等
二.1200~2400行代碼的重點:
nextTick核心源碼
1.nextTick會優(yōu)先使用microTask(微任務(wù)), 其次是macroTask(宏任務(wù))堡妒,MutationObserver是nextTick的核心皮迟,MutationObserver接口提供了監(jiān)視對DOM樹所做更改的能力伏尼,MutationObserver也是微任務(wù)爆阶,所以面試的時候如果面試官問有哪些微任務(wù)扰她,如果能答到MutationObserver是不是非常加分呢芭碍?
2.vue項目中如果需要獲取修改后的dom信息窖壕,需要通過nextTick在dom更新任務(wù)之后創(chuàng)建一個異步任務(wù)瞻讽,也就是官網(wǎng)所說的:nextTick會在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào).
三速勇、1200~2400行的代碼解讀:
/**
* 刪除屬性并在必要時觸發(fā)更改
*/
function del (target, key) {
//判斷數(shù)據(jù) 是否是undefined或者null
// 判斷數(shù)據(jù)類型是否是string烦磁,number,symbol呕乎,boolean
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
}
//判斷是否是數(shù)組猬仁,并是否是有效的數(shù)組索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 像數(shù)組尾部添加一個新數(shù)據(jù),相當于push
target.splice(key, 1);
return
}
// 聲明一個對象ob 值為該target對象中的原型上面的所有方法和屬性劲够,表明該數(shù)據(jù)加入過觀察者中
var ob = (target).__ob__;
// 如果是vue或者檢測vue被實例化的次數(shù)vmCount
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
);
return
}
//檢查對象是否具有該屬性
if (!hasOwn(target, key)) {
return
}
delete target[key];
if (!ob) {
return
}
// 觸發(fā)通知更新,通知訂閱者obj.value更新數(shù)據(jù)
ob.dep.notify();
}
// dependArray函數(shù)买雾,其實就是遍歷數(shù)組漓穿,對數(shù)組每個元素觸發(fā)dep.depend收集依賴晃危,因為不能直接對數(shù)組進行收集依賴僚饭。
// 在接觸數(shù)組時收集對數(shù)組元素的依賴關(guān)系鳍鸵,因為我們不能像屬性getter那樣攔截數(shù)組元素訪問尉间。
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
// 判斷是否存在__ob__實例哲嘲,并且每個都調(diào)用depend添加wathcer管理
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
// 遞歸完數(shù)組所有內(nèi)容眠副,直到不是數(shù)組囱怕,跳出遞歸
dependArray(e);
}
}
}
//選項 el毫别、propsData 的合并策略
// 選項覆蓋策略是處理如何合并父選項值和子選項值轉(zhuǎn)換為最終值拧烦。
// config.optionMergeStrategies定義一個合并策略恋博,其實就是vue的mixins屬性
//propsData 用在全局擴展時進行傳遞數(shù)據(jù)
var strats = config.optionMergeStrategies
{
// 在非生產(chǎn)環(huán)境下在 strats 策略對象上添加兩個策略分別是el和propsData,兩個屬性值都是函數(shù)
// 這兩個策略函數(shù)是用來合并 el 選項和 propsData 選項的
strats.el = strats.propsData = function (parent, child, vm, key) {
//如果沒有傳遞
if (!vm) {
warn(
"option \"" + key + "\" can only be used during instance " +
'creation with the `new` keyword.'
);
}
//在生產(chǎn)環(huán)境將直接使用默認的策略函數(shù) defaultStrat 來處理 el 和 propsData 這兩個選項
return defaultStrat(parent, child)
};
}
//通過_init方法可以看出债沮,策略函數(shù)中的 vm 來自于 mergeOptions 函數(shù)的第三個參數(shù)疫衩,
//mergeOptions函數(shù)傳第三個參數(shù)闷煤,策略中就拿不到vm參數(shù)鲤拿,除了_init方法中調(diào)用了mergeOptions函數(shù)近顷,其他很多地方都調(diào)用了
//Vue.extend方法中也調(diào)用了mergeoptions函數(shù)
//此時在Vue.extend方法中調(diào)用了mergeOptions函數(shù)窒升,但是沒有傳第三個參數(shù)vm饱须,所以在策略中無法拿到vm冤寿,
//就能得出mergeOptions函數(shù)是在實例化時使用new操作符走_init方法還是繼承時走的Vue.extend方法
//子組件是通過實例化子類完成的,子類是通過Vue.extend方法創(chuàng)造出來的狠角,
//綜上得出可以通過if(!vm)判斷是否是子組件
//遞歸合并兩個數(shù)據(jù)對象,to 對應(yīng)的是 childVal 產(chǎn)生的純對象丰歌,from 對應(yīng) parentVal 產(chǎn)生的純對象
function mergeData (to, from) {
//如果沒有form直接返回參數(shù)to
if (!from) { return to }
var key, toVal, fromVal;
//如果是Symbol類型
var keys = hasSymbol
? Reflect.ownKeys(from)//返回from的所以屬性key組成的數(shù)組
: Object.keys(from);//返回from的屬性key,但不包括不可枚舉的屬性
//循環(huán)from屬性key的數(shù)組
for (var i = 0; i < keys.length; i++) {
key = keys[i];
// 如果當前key為object類型眼溶,則繼續(xù)
if (key === '__ob__') { continue }
toVal = to[key];
fromVal = from[key];
//檢查對象是否具有該屬性,如果有的話就給to[key]設(shè)置fromVal屬性
if (!hasOwn(to, key)) {
set(to, key, fromVal);
} else if (
//判斷toVal堂飞,fromVal是否是通過"{}"或"new Object"創(chuàng)建,并且toVal!=fromVal
//這個方法的作用是為了跟其他的 JavaScript對象如 null绰筛,數(shù)組铝噩,宿主對象(documents)骏庸,
//DOM 等作區(qū)分具被,因為這些用 typeof 都會返回object
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal);
}
}
return to
}
//mergeDataOrFn函數(shù)永遠返回一個函數(shù)
function mergeDataOrFn (
parentVal,
childVal,
vm
) {
//如果沒有vm硬猫,則說明是子組件選項
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// 當parentVal和childVal都存在時,我們需要返回一個函數(shù)來返回兩個函數(shù)的合并結(jié)果,
// 不用檢查parentVal是否是函數(shù),因為它必須是傳遞先前合并的函數(shù)啸蜜。
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// 當合并處理的是非子組件的選項時 `data` 函數(shù)為 `mergedInstanceDataFn` 函數(shù)
return function mergedInstanceDataFn () {
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
//選項data的合并策略,在 strats 策略對象上添加 data 策略函數(shù),用來合并處理 data 選項
strats.data = function (
parentVal,
childVal,
vm
) {
//當沒有 vm 參數(shù)時蜂林,說明處理的是子組件的選項
if (!vm) {
//判斷childVal是不是函數(shù)
if (childVal && typeof childVal !== 'function') {
warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
);
//如果不是函數(shù)直接返回 parentVal
return parentVal
}
//無論是否為子組件選項都會調(diào)用mergeDataOrFn方法噪叙,但是如果是子組件選項會傳第三個參數(shù)vm,
// 由此可在mergeDataOrFn函數(shù)內(nèi)判斷是否為子組件選項
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};
// 生命周期鉤子選項的合并策略
function mergeHook (
parentVal,
childVal
) {
//三目運算符:判斷是否有childVal參數(shù)睁蕾,如果有的話判斷是否有parentVal參數(shù)子眶,
// 如果有parentVal參數(shù)就將childVal與parentVal合并,
//如果沒有 parentVal 則判斷 childVal 是不是一個數(shù)組
//如果是數(shù)組類型粤咪,則直接返回寥枝,若不是則將childVal轉(zhuǎn)成數(shù)組脉顿,說明生命周期鉤子是可以寫成數(shù)組的并會按照數(shù)組順序依次執(zhí)行
var res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal;
return res
? dedupeHooks(res)
: res
}
//這個函數(shù)主要用來處理生命周期鉤子的唯一
function dedupeHooks (hooks) {
var res = [];
for (var i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res
}
// LIFECYCLE_HOOKS 常量實際上是由與生命周期鉤子同名的字符串組成的數(shù)組
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});
// 資源(assets)選項的合并策略
function mergeAssets (
parentVal,
childVal,
vm,
key
) {
創(chuàng)建parentVal新對象
var res = Object.create(parentVal || null);
// 判斷是否有childVal參數(shù)
if (childVal) {
//主要就是為了判斷childVal是否為存粹的對象
assertObjectType(key, childVal, vm);
return extend(res, childVal)
} else {
return res
}
}
//用來遍歷 ASSET_TYPES 常量(component艾疟、directive蔽莱、filter)
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets;
});
// 選項 watch 的合并策略,合并處理 watch 選項的
strats.watch = function (
parentVal,
childVal,
vm,
key
) {
// 在 Firefox 瀏覽器中 Object.prototype 擁有原生的 watch 函數(shù),容易造成迷惑,
// 所以當發(fā)現(xiàn)組件選項是瀏覽器原生的 watch 時盗冷,那說明用戶并沒有提供 Vue 的 watch 選項仪糖,直接重置為 undefined
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
//如果沒有childVal(即組件選項是否有 watch 選項),則返回parentVal新對象
if (!childVal) { return Object.create(parentVal || null) }
{
//判斷childVal是否為存粹的對象
assertObjectType(key, childVal, vm);
}
//parentVal,則直接返回childVal,即直接使用組件選項的 watch
if (!parentVal) { return childVal }
var ret = {};
//將parentVal插入ret對象
extend(ret, parentVal);
//parentVal,childVal都存在,就需要做合并處理
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
//如果parent存在,則將child與parent合并,如果 parent 不存在蟆湖,直接將 child 轉(zhuǎn)為數(shù)組返回
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
/**
* 選項 props隅津、methods伦仍、inject充蓝、computed 的合并策略
*/
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal,
childVal,
vm,
key
) {
//如果有childVal參數(shù),并且不是生產(chǎn)環(huán)境,則需要判斷childVal是否為存粹的對象
if (childVal && "development" !== 'production') {
assertObjectType(key, childVal, vm);
}
//如果沒有parentVal參數(shù),則直接返回childVal,即直接使用對應(yīng)的組件選項
if (!parentVal) { return childVal }
var ret = Object.create(null);
extend(ret, parentVal);
if (childVal) { extend(ret, childVal); }
return ret
};
//選項 provide 的合并策略,與data相同
strats.provide = mergeDataOrFn;
/*對于 el、propsData 選項使用默認的合并策略 defaultStrat。
對于 data 選項确买,使用 mergeDataOrFn 函數(shù)進行處理湾趾,最終結(jié)果是 data 選項將變成一個函數(shù)搀缠,且該函數(shù)的執(zhí)行結(jié)果為真正的數(shù)據(jù)對象艺普。
對于 生命周期鉤子 選項歧譬,將合并成數(shù)組瑰步,使得父子選項中的鉤子函數(shù)都能夠被執(zhí)行
對于 directives缩焦、filters 以及 components 等資源選項袁滥,父子選項將以原型鏈的形式被處理呻拌,正是因為這樣我們才能夠在任何地方都使用內(nèi)置組件藐握、指令等垃喊。
對于 watch 選項的合并處理本谜,類似于生命周期鉤子,如果父子選項都有相同的觀測字段陌知,將被合并為數(shù)組仆葡,這樣觀察者都將被執(zhí)行沿盅。
對于 props腰涧、methods窖铡、inject万伤、computed 選項呜袁,父選項始終可用,但是子選項會覆蓋同名的父選項字段虹钮。
對于 provide 選項芙粱,其合并策略使用與 data 選項相同的 mergeDataOrFn 函數(shù)春畔。
以上沒有提及到的選項都將使默認選項 defaultStrat律姨。*/
var defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
};
// checkComponents函數(shù)择份,校驗child對象中組件參數(shù)components中的組件名是否符合規(guī)范
function checkComponents (options) {
for (var key in options.components) {
validateComponentName(key);
}
}
//組件名的校驗
function validateComponentName (name) {
if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}
// 對props的屬性進行一些規(guī)范化
function normalizeProps (options, vm) {
var props = options.props;
if (!props) { return }
var res = {};
var i, val, name;
//如果props是數(shù)組,則將遍歷props,將props轉(zhuǎn)成對象,例如:{ title: {type: null} }
if (Array.isArray(props)) {
i = props.length;
while (i--) {
val = props[i];
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else {
warn('props must be strings when using array syntax.');
}
}
//判斷props是否為純粹的對象
} else if (isPlainObject(props)) {
for (var key in props) {
val = props[key];
name = camelize(key);//將key駝峰化
res[name] = isPlainObject(val)
? val
: { type: val };//例如:props: {type: String}
}
} else {
warn(
"Invalid value for option \"props\": expected an Array or an Object, " +
"but got " + (toRawType(props)) + ".",
vm
);
}
options.props = res;
}
// 對inject的屬性進行一些規(guī)范化,與normalizeProps差不多
function normalizeInject (options, vm) {
var inject = options.inject;
if (!inject) { return }
var normalized = options.inject = {};
//如果inject是數(shù)組,則將循環(huán)inject,將inject轉(zhuǎn)成對象,例如:{ title: {form: null} }
if (Array.isArray(inject)) {
for (var i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] };
}
//如果inject是純粹的對象,則循環(huán)inject
} else if (isPlainObject(inject)) {
for (var key in inject) {
var val = inject[key];
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val };//例如inject: {'title': {from: 'title'} }
}
} else {
warn(
"Invalid value for option \"inject\": expected an Array or an Object, " +
"but got " + (toRawType(inject)) + ".",
vm
);
}
}
// 對directives的屬性進行一些規(guī)范化,例如directives: {change:{bind(){...},update(){...}}
function normalizeDirectives (options) {
var dirs = options.directives;
if (dirs) {
for (var key in dirs) {
var def$$1 = dirs[key];
//def$$1必須為函數(shù)
if (typeof def$$1 === 'function') {
dirs[key] = { bind: def$$1, update: def$$1 };
}
}
}
}
//判斷是否為純粹的對象
function assertObjectType (name, value, vm) {
if (!isPlainObject(value)) {
warn(
"Invalid value for option \"" + name + "\": expected an Object, " +
"but got " + (toRawType(value)) + ".",
vm
);
}
}
// 合并策略函數(shù),參數(shù)選項合并參數(shù);parent:父實例參數(shù);child:子實例參數(shù);vm:vue實例參數(shù),實例化過程中被調(diào)用.
function mergeOptions (
parent,
child,
vm
) {
{
//校驗child對象中組件參數(shù)components中的組件名是否符合規(guī)范
checkComponents(child);
}
//判斷child是否為函數(shù)
if (typeof child === 'function') {
child = child.options;
}
// Props,Inject,Directives的規(guī)范化
normalizeProps(child, vm);
normalize(child, vm);
normalizeDirectives(child);
//只有合并過的選項會帶有_base屬性,判斷child是否存在_base屬性,如果存在則說明已經(jīng)被合并處理過
if (!child._base) {
//將extend和mixins再通過mergeOptions函數(shù)與parent合并
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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;
//遍歷parent執(zhí)行mergeField
for (key in parent) {
mergeField(key);
}
// 遍歷child,當parent沒有key的時候執(zhí)行mergeField
// 如果有key屬性拔创,就不需要合并剩燥,因為上一步已經(jīng)合并到options
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
// 該函數(shù)主要是通過key獲取到對應(yīng)的合并策略函數(shù)躏吊,然后執(zhí)行合并比伏,賦值給options[key]
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
//用于獲取Vue.extend里面定義的構(gòu)造函數(shù)
function resolveAsset (
options,
type,
id,
warnMissing
) {
if (typeof id !== 'string') {
return
}
var assets = options[type];
// 檢查assets中是否包含id鍵,如果有的話直接返回
if (hasOwn(assets, id)) { return assets[id] }
//規(guī)范鍵的駝峰命名
var camelizedId = camelize(id);
// 如果上一個條件不成立,那么將鍵轉(zhuǎn)成駝峰后再判斷
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
//將駝峰后的鍵的首字母大寫
var PascalCaseId = capitalize(camelizedId);
// 如果以上條件不成立,那么將鍵轉(zhuǎn)成駝峰后再將首字母轉(zhuǎn)成大寫,再次判斷
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
return res
}
//檢查一下我們傳遞的數(shù)據(jù)是否滿足 prop的定義規(guī)范
function validateProp (
key,
propOptions,
propsData,
vm
) {
var prop = propOptions[key];//獲取propOptions中對應(yīng)的值
var absent = !hasOwn(propsData, key);//如果propsData沒有這個key,則absent為true
var value = propsData[key];//獲取propsData中對應(yīng)的值
//判斷prop.type是否為布爾類型
var booleanIndex = getTypeIndex(Boolean, prop.type);
if (booleanIndex > -1) {
//如果absent為true,并且prop中不包含'default'
if (absent && !hasOwn(prop, 'default')) {
value = false;
// 如果value為空字符串,獲取value與轉(zhuǎn)成駝峰后的key相等
} else if (value === '' || value === hyphenate(key)) {
// 判斷prop.type是否為String類型
var stringIndex = getTypeIndex(String, prop.type);
// 如果為stringIndex類型,或者booleanIndex的優(yōu)先級比stringIndex的高
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true;
}
}
}
// 當value全等于undefined時,調(diào)用getPropDefaultValue方法取prop的默認值
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key);
//shouldObserve默認值為true
var prevShouldObserve = shouldObserve;
//改變shouldObserve的值為true
toggleObserving(true);
//判斷value是否為一個對象
observe(value);
//改變shouldObserve的值為prevShouldObserve的值
toggleObserving(prevShouldObserve);
}
{
assertProp(prop, key, value, vm, absent);
}
return value
}
//獲取prop的默認值
function getPropDefaultValue (vm, prop, key) {
//如果prop中不包含'default'屬性,則直接返回undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
var def = prop.default;
//如果def為object類型,則發(fā)出警告
if (isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
vm
);
}
// vue實例存在悠菜,組件實例上的props數(shù)據(jù)存在悔醋,父組件傳進來的props中沒有該key值的屬性芬骄,
// 如果vm實例上_props有账阻,就返回_props上的屬性淘太,_props是之前通過proxy代理的
//是vue做的一點優(yōu)化蒲牧,由于函數(shù)每次返回的對象都是一個新的引用造成,當組件更新的時候晒屎,
//為了避免不必要watcher update而設(shè)置
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
//如果是function 則調(diào)用def.call(vm),否則就返回default屬性對應(yīng)的值
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
// 校驗是否通過,先驗證required再驗證type
function assertProp (
prop,
name,
value,
vm,
absent
) {
//如果必傳,但若當前為undefined時,則發(fā)出警告
if (prop.required && absent) {
warn(
'Missing required prop: "' + name + '"',
vm
);
return
}
//如果非必傳時并且value為null,則直接返回
if (value == null && !prop.required) {
return
}
var type = prop.type;
var valid = !type || type === true;
var expectedTypes = [];
//type必須為數(shù)組類型
if (type) {
if (!Array.isArray(type)) {
type = [type];
}
for (var i = 0; i < type.length && !valid; i++) {
var assertedType = assertType(value, type[i], vm);
expectedTypes.push(assertedType.expectedType || '');
valid = assertedType.valid;
}
}
var haveExpectedTypes = expectedTypes.some(function (t) { return t; });
//如果valid為false,并且expectedTypes有值,則prop 的值 value 與 prop 定義的類型都不匹配,則輸出警告
if (!valid && haveExpectedTypes) {
warn(
getInvalidTypeMessage(name, value, expectedTypes),
vm
);
return
}
// 判斷有沒有自定義的校驗器蕴轨,如果有則執(zhí)行橙弱,并在校驗器返回false時觸發(fā)warn
var validator = prop.validator;
if (validator) {
if (!validator(value)) {
warn(
'Invalid prop: custom validator check failed for prop "' + name + '".',
vm
);
}
}
}
//props類型檢查
var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol|BigInt)$/;
// 獲取斷言的結(jié)果,直到遍歷完成或者是 valid 為 true 的時候跳出循環(huán)
function assertType (value, type, vm) {
var valid;
// 獲取 prop 期望的類型 expectedType,然后對比prop的值value是否和expectedType 匹配
var expectedType = getType(type);
if (simpleCheckRE.test(expectedType)) {
var t = typeof value;
valid = t === expectedType.toLowerCase();
// value 的類型必須是 type 數(shù)組里的其中之一
if (!valid && t === 'object') {
valid = value instanceof type;
}
//判斷value是否為純粹的對象
} else if (expectedType === 'Object') {
valid = isPlainObject(value);
} else if (expectedType === 'Array') {
//判斷value是否為數(shù)組
valid = Array.isArray(value);
} else {
try {
//判斷value是否為數(shù)組
valid = value instanceof type;
} catch (e) {
warn('Invalid prop type: "' + String(type) + '" is not a constructor', vm);
valid = false;
}
}
return {
valid: valid,
expectedType: expectedType
}
}
//函數(shù)檢查
var functionTypeCheckRE = /^\s*function (\w+)/;
//將fn參數(shù)轉(zhuǎn)成字符串,然后在字符串內(nèi)查找function
function getType (fn) {
var match = fn && fn.toString().match(functionTypeCheckRE);
return match ? match[1] : ''
}
// 判斷是否是同一個類型
function isSameType (a, b) {
return getType(a) === getType(b)
}
//找到 type 和 expectedTypes 匹配的索引并返回
function getTypeIndex (type, expectedTypes) {
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
for (var i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i
}
}
return -1
}
//生成警告信息
function getInvalidTypeMessage (name, value, expectedTypes) {
var message = "Invalid prop: type check failed for prop \"" + name + "\"." +
" Expected " + (expectedTypes.map(capitalize).join(', '));
var expectedType = expectedTypes[0];
var receivedType = toRawType(value);
// 檢查是否需要指定接收值
if (
expectedTypes.length === 1 &&
isExplicable(expectedType) &&
isExplicable(typeof value) &&
!isBoolean(expectedType, receivedType)
) {
message += " with value " + (styleValue(value, expectedType));
}
message += ", got " + receivedType + " ";
// 檢查是否需要指定接收值
if (isExplicable(receivedType)) {
message += "with value " + (styleValue(value, receivedType)) + ".";
}
return message
}
function styleValue (value, type) {
if (type === 'String') {
return ("\"" + value + "\"")
} else if (type === 'Number') {
return ("" + (Number(value)))
} else {
return ("" + value)
}
}
var EXPLICABLE_TYPES = ['string', 'number', 'boolean'];
function isExplicable (value) {
//將value轉(zhuǎn)為小寫,然后判斷value是否是'string', 'number', 'boolean'的其中一種類型
return EXPLICABLE_TYPES.some(function (elem) { return value.toLowerCase() === elem; })
}
//判斷是否為boolean類型
function isBoolean () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; })
}
/* 捕捉異常 */
// 首先獲取到報錯的組件,之后遞歸查找當前組件的父組件蛀缝,依次調(diào)用errorCaptured 方法屈梁。
// 在遍歷調(diào)用完所有 errorCaptured 方法在讶、或 errorCaptured 方法有報錯時构哺,調(diào)用 globalHandleError 方法
function handleError (err, vm, info) {
// Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
// See: https://github.com/vuejs/vuex/issues/1505
pushTarget();
try {
// vm指當前報錯的組件實例
if (vm) {
var cur = vm;
// 遞歸查找當前組件的父組件遮婶,依次調(diào)用errorCaptured 方法旗扑。
// 在遍歷調(diào)用完所有 errorCaptured 方法臀防、或 errorCaptured 方法有報錯時边败,調(diào)用 globalHandleError 方法
while ((cur = cur.$parent)) {
var hooks = cur.$options.errorCaptured;
// 判斷是否存在errorCaptured鉤子函數(shù)
if (hooks) {
for (var i = 0; i < hooks.length; i++) {
try {
var capture = hooks[i].call(cur, err, vm, info) === false;
if (capture) { return }
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook');
}
}
}
}
}
globalHandleError(err, vm, info);
} finally {
popTarget();
}
}
// 異步錯誤處理
function invokeWithErrorHandling (
handler,
context,
args,
vm,
info
) {
var res;
try {
//根據(jù)不同參數(shù)選擇不同的處理函數(shù)
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
res._handled = true;
}
} catch (e) {
handleError(e, vm, info);
}
return res
}
// 調(diào)用全局的 errorHandler 方法致燥,生產(chǎn)環(huán)境下會使用 console.error 在控制臺中輸出
function globalHandleError (err, vm, info) {
// 獲取全局配置嫌蚤,判斷是否設(shè)置處理函數(shù)
if (config.errorHandler) {
try {
// 執(zhí)行設(shè)置的全局錯誤處理函數(shù)
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
// 如果開發(fā)者在errorHandler函數(shù)中手動拋出同樣錯誤信息throw err判斷err信息是否相等脱吱,避免log兩次
// 如果拋出新的錯誤信息將會一起log輸出
if (e !== err) {
logError(e, null, 'config.errorHandler');
}
}
}
logError(err, vm, info);
}
// 判斷環(huán)境箱蝠,選擇不同的拋錯方式宦搬。瀏覽器或微信環(huán)境,并且console的類型為undefied時,打印error
function logError (err, vm, info) {
{
warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm);
}
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
console.error(err);
} else {
throw err
}
}
//接下來是nextTick的核心代碼
// 使用 MicroTask(微任務(wù)) 的標識符
var isUsingMicroTask = false;
var callbacks = [];
var pending = false;
//依次同步執(zhí)行callbacks中回調(diào)
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
/*在vue2.5之前的版本中一罩,nextTick基本上基于 micro task 來實現(xiàn)的,但是在某些情況下 micro task 具有太高的優(yōu)先級四瘫,
并且可能在連續(xù)順序事件之間(例如#4521找蜜,#6690)或者甚至在同一事件的事件冒泡過程中之間觸發(fā)(#6566)洗做。
但是如果全部都改成 macro task诚纸,對一些有重繪和動畫的場景也會有性能影響畦徘,如 issue #6813井辆。
vue2.5之后版本提供的解決辦法是默認使用 micro task杯缺,但在需要時(例如在v-on附加的事件處理程序中)強制使用 macro task*/
var timerFunc;
//nextTick行為利用了可以訪問的微任務(wù)隊列
//通過本機Promise.then或MutationObserver萍肆。
//MutationObserver得到了更廣泛的支持匾鸥,但是它在
//iOS中的UIWebView>=9.3.3勿负,當touch事件處理程序中觸發(fā)時,幾次后會完全停止工作
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
//在有問題的UIWebViews中奴愉,Promise.then并沒有完全崩潰锭硼,但是
//它可能會陷入一種奇怪的狀態(tài)檀头,即回調(diào)被推到
//微任務(wù)隊列,但在瀏覽器
//需要做一些其他工作搭独,例如處理計時器牙肝。所以我們可以
//通過添加計時器“強制”刷新微任務(wù)隊列配椭。
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
// MutationObserver的作用通過它創(chuàng)建一個觀察者對象股缸,這個對象會監(jiān)聽某個DOM元素,并在它的DOM樹發(fā)生變化時執(zhí)行我們提供的回調(diào)函數(shù)
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
// 聲明 MO 和回調(diào)函數(shù)
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
// 監(jiān)聽 textNode 這個文本節(jié)點替劈,一旦文本改變則觸發(fā)回調(diào)函數(shù) nextTickHandler
observer.observe(textNode, {
characterData: true
});
// 每次執(zhí)行 timeFunc 都會讓文本在 1 和 0 間切換
timerFunc = function () {
counter = (counter + 1) % 2;
//把該數(shù)據(jù)值賦值到data屬性上面去陨献,如果data屬性發(fā)生改變了眨业,就會重新渲染頁面
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// setImmediate使用宏任務(wù)
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
//如果不支持MutationObserver龄捡,則使用settimeout
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function nextTick (cb, ctx) {
var _resolve;
// 傳入的cb是否是函數(shù)聘殖,ctx參數(shù)是否是一個對象奸腺,如果cb是一個函數(shù)的話帮非,使用cb.call(ctx)
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
//如果pending為true讹蘑,表明本輪事件循環(huán)中已經(jīng)執(zhí)行過 timerFunc(nextTickHandler, 0)
if (!pending) {
pending = true;
timerFunc();
}
//判斷是否有Promise對象
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
/* */
var mark;
var measure;
// 此方法是獲取標簽在瀏覽器加載的時間快照
{
//在瀏覽器環(huán)境
var perf = inBrowser && window.performance;
//performance.mark方法在瀏覽器的性能條目緩沖區(qū)中創(chuàng)建一個具有給定名稱的緩沖區(qū)庄岖,
//performance.measure在瀏覽器的兩個指定標記(分別稱為起始標記和結(jié)束標記)之間的性能條目緩沖區(qū)中創(chuàng)建一個命名
//performance.measure從瀏覽器的performance entry 緩存中移除聲明的標記
if (
perf &&
perf.mark &&
perf.measure &&
perf.clearMarks &&
perf.clearMeasures
) {
mark = function (tag) { return perf.mark(tag); };
measure = function (name, startTag, endTag) {
perf.measure(name, startTag, endTag);
perf.clearMarks(startTag);
perf.clearMarks(endTag);
// perf.clearMeasures(name)
};
}
}
/* not type checking this file because flow doesn't play well with Proxy */
//初始化代理
var initProxy;
{
//makeMap 方法將字符串切割,放到map中邦尊,用于校驗其中的某個字符串是否存在(區(qū)分大小寫)于map中
var allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,' +
'require' // for Webpack/Browserify
);
// 不存在蝉揍,未定義的屬性又沾,方法被使用給出警告
var warnNonPresent = function (target, key) {
warn(
"Property or method \"" + key + "\" is not defined on the instance but " +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
);
};
//用于檢測屬性 key 的聲明方法杖刷,是否是 $ 或者 _ 開頭的滑燃,如果是表窘,會給出警告
var warnReservedPrefix = function (target, key) {
warn(
"Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
'prevent conflicts with Vue internals. ' +
'See: https://vuejs.org/v2/api/#data',
target
);
};
var hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy);
//當前環(huán)境中的proxy可用
if (hasProxy) {
//某個字符串是否存在map中
var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact');
//config.keyCodes自定義按鍵修飾符
config.keyCodes = new Proxy(config.keyCodes, {
set: function set (target, key, value) {
//key是否存在map中
if (isBuiltInModifier(key)) {
warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key));
return false
} else {
target[key] = value;
return true
}
}
});
}
//這個函數(shù)主要是提示錯誤信息瘤袖,在開發(fā)者錯誤的調(diào)用vm屬性時捂敌,提供提示作用
var hasHandler = {
has: function has (target, key) {
var has = key in target;
var isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
if (!has && !isAllowed) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return has || !isAllowed
}
};
// getHandler方法主要是針對讀取代理對象的某個屬性時進行的操作黍匾。
// 當訪問的屬性不是string類型或者屬性值在被代理的對象上不存在锐涯,則拋出錯誤提示纹腌,否則就返回該屬性值
var getHandler = {
get: function get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return target[key]
}
};
//初始化代理
initProxy = function initProxy (vm) {
// 通過判斷hasProxy,來執(zhí)行不同的處理邏輯
if (hasProxy) {
// 如果options上存在render屬性涎劈,且render屬性上存在_withStripped屬性蛛枚,
// 則proxy的traps(traps其實也就是自定義方法)采用getHandler方法,否則采用hasHandler方法
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler
: hasHandler;
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
}
var seenObjects = new _Set();
// 遞歸遍歷對象以調(diào)用所有已轉(zhuǎn)換的getter蹦浦,以便對象中的每個嵌套屬性,作為'deep'依賴項收集
//這個函數(shù)主要用來進行深度監(jiān)聽
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
// val是否是數(shù)組 或者是已經(jīng)凍結(jié)對象或者是VNode實例
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 只有object和array才有__ob__屬性
if (val.__ob__) {
var depId = val.__ob__.dep.id;//手動依賴收集器的id
if (seen.has(depId)) {
return
}
seen.add(depId);
}
if (isA) {
i = val.length;
// 遞歸觸發(fā)每一項的get進行依賴收集
while (i--) { _traverse(val[i], seen); }
} else {
keys = Object.keys(val);
i = keys.length;
// 遞歸觸發(fā)子屬性的get進行依賴收集
while (i--) { _traverse(val[keys[i]], seen); }
}
}
// 主要用于將傳入的帶有特殊前綴的事件修飾符分解為具有特定值的事件對象
// name屬性:事件名稱
// once屬性:是否一次性執(zhí)行
// capture屬性:是否捕獲事件
// passive屬性:是否使用被動模式
var normalizeEvent = cached(function (name) {
var passive = name.charAt(0) === '&';
name = passive ? name.slice(1) : name;
var once$$1 = name.charAt(0) === '~';
name = once$$1 ? name.slice(1) : name;
var capture = name.charAt(0) === '!';
name = capture ? name.slice(1) : name;
return {
name: name,
once: once$$1,
capture: capture,
passive: passive
}
});
//與normalizeEvent一起完成更新監(jiān)聽器的實現(xiàn),是真正執(zhí)行事件處理器調(diào)用的過程
function createFnInvoker (fns, vm) {
function invoker () {
var arguments$1 = arguments;
// invoker.fns用來存放所傳入的處理器
var fns = invoker.fns;
if (Array.isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
//處理器數(shù)組的調(diào)用
invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
}
} else {
// 單個處理器的調(diào)用
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
}
}
invoker.fns = fns;
return invoker
}
//這個函數(shù)主要是修改監(jiān)聽配置溉贿,遍歷on事件對新節(jié)點事件綁定注冊事件顽照,對舊節(jié)點移除事件監(jiān)聽
function updateListeners (
on,
oldOn,
add,
remove$$1,
createOnceHandler,
vm
) {
var name, def$$1, cur, old, event;
// 遍歷on事件對新節(jié)點事件綁定注冊事件
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
//執(zhí)行真正注冊事件的執(zhí)行函數(shù)尼酿,add的實現(xiàn)原理利用了原生 DOM 的 addEventListener
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
//遍歷舊節(jié)點裳擎,對舊節(jié)點移除事件監(jiān)聽
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
// 把insert作為一個hooks屬性保存到對應(yīng)的Vnode的data上面鹿响,當該Vnode插入到父節(jié)點后會調(diào)用該hooks
function mergeVNodeHook (def, hookKey, hook) {
if (def instanceof VNode) {
def = def.data.hook || (def.data.hook = {});
}
var invoker;
var oldHook = def[hookKey];
function wrappedHook () {
hook.apply(this, arguments);
// 刪除合并的鉤子以確保它只被調(diào)用一次并防止內(nèi)存泄漏
remove(invoker.fns, wrappedHook);
}
if (isUndef(oldHook)) {
// 沒有已經(jīng)存在的hook
invoker = createFnInvoker([wrappedHook]);
} else {
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
// 已經(jīng)有一個invoker(已經(jīng)是合并的調(diào)用程序)
invoker = oldHook;
invoker.fns.push(wrappedHook);
} else {
invoker = createFnInvoker([oldHook, wrappedHook]);
}
}
invoker.merged = true;
def[hookKey] = invoker;
}
// 獲取原始值
function extractPropsFromVNodeData (
data,
Ctor,
tag
) {
// 獲取組件的定義的props對象,這里只提取原始值博投。驗證和默認值在子組件本身中處理。
var propOptions = Ctor.options.props;
if (isUndef(propOptions)) {
return
}
var res = {};
var attrs = data.attrs;//獲取data的attrs屬性
var props = data.props;//獲取data的props屬性
// 如果data有定義了attrs或者props屬性
if (isDef(attrs) || isDef(props)) {
//遍歷組件的props屬性
for (var key in propOptions) {
// 如果key是是駝峰字符串听怕,則轉(zhuǎn)換為-格式
var altKey = hyphenate(key);
{
//轉(zhuǎn)換為小寫格式
var keyInLowerCase = key.toLowerCase();
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
tip(
"Prop \"" + keyInLowerCase + "\" is passed to component " +
(formatComponentName(tag || Ctor)) + ", but the declared prop name is" +
" \"" + key + "\". " +
"Note that HTML attributes are case-insensitive and camelCased " +
"props need to use their kebab-case equivalents when using in-DOM " +
"templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"."
);
}
}
// 調(diào)用checkProp從props或attrs里拿對應(yīng)的屬性
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false);
}
}
return res
}